--- title: Nodes keywords: fastai sidebar: home_sidebar summary: "A node is a single control unit representing a feedback control loop. " description: "A node is a single control unit representing a feedback control loop. " nb_path: "nbs/03_nodes.ipynb" ---
%load_ext autoreload
%autoreload 2
A node comprises four functions, reference, perceptual, comparator and output. Executing the node will run each of the functions in the order indicated above and return the output value.
The functions can actually be a collection of functions, each executed in the order they are added. This allows a chain of functions in case pre-processing is required, or post-processing in the case of the output.
#node.summary()
node = PCTNode()
node.summary()
That creates a node with default functions. Those are, a constant of 1 for the reference, a variable, with initial value 0, for the perception and a proportional function for the output, with a gain of 10.
A node can also be created by providing a name, and setting the history to True. The latter means that the values of all the functions are recorded during execution, which is useful for plotting the data later, as can be seen below.
dynamic_module_import( 'pct.functions', 'Constant')
reference = Constant(1)
namespace=reference.namespace
node = PCTNode(name="mypctnode", history=True, reference = reference, output=Proportional(10, namespace=namespace), namespace=namespace)
node.summary()
Another way of creating a node is by first declaring the functions you want and passing them into the constructor.
UniqueNamer.getInstance().clear()
r = Variable(0, name="velocity_reference")
p = Constant(10, name="constant_perception")
o = Integration(10, 100, name="integrator")
integratingnode = PCTNode(reference=r, perception=p, output=o, name="integratingnode", history=True)
Yet another way to create a node is from a text configuration.
config_node = PCTNode.from_config({ 'name': 'mypctnode',
'refcoll': {'0': {'type': 'Proportional', 'name': 'proportional', 'value': 0, 'links': {}, 'gain': 10}},
'percoll': {'0': {'type': 'Variable', 'name': 'velocity', 'value': 0.2, 'links': {}}},
'comcoll': {'0': {'type': 'Subtract', 'name': 'subtract', 'value': 1, 'links': {0: 'constant', 1: 'velocity'}}},
'outcoll': {'0': {'type': 'Proportional', 'name': 'proportional', 'value': 10, 'links': {0: 'subtract'}, 'gain': 10}}}, namespace=namespace)
integratingnode.summary()
assert integratingnode.get_config() == {'type': 'PCTNode', 'name': 'integratingnode', 'refcoll': {'0': {'type': 'Variable', 'name': 'velocity_reference', 'value': 0, 'links': {}}}, 'percoll': {'0': {'type': 'Constant', 'name': 'constant_perception', 'value': 10, 'links': {}}}, 'comcoll': {'0': {'type': 'Subtract', 'name': 'subtract', 'value': 0, 'links': {0: 'velocity_reference', 1: 'constant_perception'}}}, 'outcoll': {'0': {'type': 'Integration', 'name': 'integrator', 'value': 0, 'links': {0: 'subtract'}, 'gain': 10, 'slow': 100}}}
integratingnode.get_config()
A node can also be viewed graphically as a network of connected nodes.
import os
if os.name=='nt':
integratingnode.draw(node_size=2000, figsize=(8,4))
def velocity_model(velocity, force , mass):
velocity = velocity + force / mass
return velocity
mass = 50
force = 0
In the following cell we start with a velocity of zero. The node is run once (second line), the output of which is the force to apply in the world velocity_model. That returns the updated velocity which we pass back into the node to be used in the next iteration of the loop.
velocity=0
force = node()
velocity = velocity_model(velocity, force, mass)
node.set_perception_value(velocity)
print(force)
assert force == 10
The node can be run in a loop as shown below. With verbose set to True the output of each loop will be printed to the screen.
pctnode = PCTNode(history=True)
pctnode.set_function_name("perception", "velocity")
pctnode.set_function_name("reference", "reference")
for i in range(40):
print(i, end=" ")
force = pctnode(verbose=True)
vel = velocity_model(pctnode.get_perception_value(), force, mass)
pctnode.set_perception_value(vel)
Save a node to file.
import json
integratingnode.save("inode.json")
Create a node from file.
nnode = PCTNode.load("inode.json")
print(nnode.get_config())
As the history of the variable pctnode was set to True the data is available for analysis. It can be plotted with python libraries such as matplotlib or plotly. Here is an example with the latter.
The graph shows the changing perception values as it is controlled to match the reference value.
import plotly.graph_objects as go
fig = go.Figure(layout_title_text="Velocity Goal")
fig.add_trace(go.Scatter(y=pctnode.history.data['refcoll']['reference'], name="ref"))
fig.add_trace(go.Scatter(y=pctnode.history.data['percoll']['velocity'], name="perc"))
This following code is only for the purposes of displaying image of the graph generated by the above code.
from IPython.display import Image
Image(url='http://www.perceptualrobots.com/wp-content/uploads/2020/08/pct_node_plot.png')
#from nbdev import *
#notebook2script()