Nodes

A node is a single control unit representing a feedback control loop.

Overview

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.


source

ControlUnitIndices

 ControlUnitIndices (value, names=None, module=None, qualname=None,
                     type=None, start=1)

An enumeration.


source

PCTNode

 PCTNode (reference=None, perception=None, comparator=None, output=None,
          default=True, name='pctnode', history=False, build_links=False,
          mode=0, namespace=None, **pargs)

A single PCT controller.

#node = PCTNode()
#node.summary()

source

PCTNodeData

 PCTNodeData (name='pctnodedata')

Data collected for a PCTNode

Creating a Node

A node can be created simply.

node = PCTNode()
node.summary()
pctnode PCTNode e2723476-b938-11ee-9ede-5c879c15de65
----------------------------
REF: constant Constant | 0 
PER: variable Variable | 0 
COM: subtract Subtract | 0 | links  constant variable 
OUT: proportional Proportional | gain 1 | 0 | links  subtract 
----------------------------

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()
mypctnode PCTNode e281ebf9-b938-11ee-944f-5c879c15de65
----------------------------
REF: constant Constant | 1 
PER: variable Variable | 0 
COM: subtract Subtract | 0 | links  constant variable 
OUT: proportional Proportional | gain 10 | 0 | links  subtract 
----------------------------

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}}})
# config_node = PCTNode.from_config({ 'name': 'mypctnode1', 
#     '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)

Viewing Nodes

The details of a node can be viewed in a number of ways, which is useful for checking the configuration. The summary method prints to the screen. The get_config method returns a string in a JSON format.

integratingnode.summary()
integratingnode PCTNode e28f596d-b938-11ee-a963-5c879c15de65
----------------------------
REF: velocity_reference Variable | 0 
PER: constant_perception Constant | 10 
COM: subtract Subtract | 0 | links  velocity_reference constant_perception 
OUT: integrator Integration | gain 10 slow 100  | 0 | links  subtract 
----------------------------
#print(integratingnode.get_config())
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()
{'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}}}

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))

Running a Node

For the purposes of this example we first create a function which is a very basic model of the physical environment. It defines how the world behaves when we pass it the output of the control system.

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
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)
0 0.000 0.000 0.000 0.000 
1 0.000 0.000 0.000 0.000 
2 0.000 0.000 0.000 0.000 
3 0.000 0.000 0.000 0.000 
4 0.000 0.000 0.000 0.000 
5 0.000 0.000 0.000 0.000 
6 0.000 0.000 0.000 0.000 
7 0.000 0.000 0.000 0.000 
8 0.000 0.000 0.000 0.000 
9 0.000 0.000 0.000 0.000 
10 0.000 0.000 0.000 0.000 
11 0.000 0.000 0.000 0.000 
12 0.000 0.000 0.000 0.000 
13 0.000 0.000 0.000 0.000 
14 0.000 0.000 0.000 0.000 
15 0.000 0.000 0.000 0.000 
16 0.000 0.000 0.000 0.000 
17 0.000 0.000 0.000 0.000 
18 0.000 0.000 0.000 0.000 
19 0.000 0.000 0.000 0.000 
20 0.000 0.000 0.000 0.000 
21 0.000 0.000 0.000 0.000 
22 0.000 0.000 0.000 0.000 
23 0.000 0.000 0.000 0.000 
24 0.000 0.000 0.000 0.000 
25 0.000 0.000 0.000 0.000 
26 0.000 0.000 0.000 0.000 
27 0.000 0.000 0.000 0.000 
28 0.000 0.000 0.000 0.000 
29 0.000 0.000 0.000 0.000 
30 0.000 0.000 0.000 0.000 
31 0.000 0.000 0.000 0.000 
32 0.000 0.000 0.000 0.000 
33 0.000 0.000 0.000 0.000 
34 0.000 0.000 0.000 0.000 
35 0.000 0.000 0.000 0.000 
36 0.000 0.000 0.000 0.000 
37 0.000 0.000 0.000 0.000 
38 0.000 0.000 0.000 0.000 
39 0.000 0.000 0.000 0.000 

Save and Load

Save a node to file.

import json
integratingnode.save("inode.json")

Create a node from file.

nnode = PCTNode.load("inode.json")
nnode.summary()
print(nnode.get_config())
integratingnode PCTNode 576b00b4-b939-11ee-9e11-5c879c15de65
----------------------------
REF: velocity_reference Variable | 0 
PER: constant_perception Constant | 10 
COM: subtract Subtract | 0 | links  velocity_reference constant_perception 
OUT: integrator Integration | gain 10 slow 100  | 0 | links  subtract 
----------------------------
{'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}}}
print(nnode.get_summary())
0.000 10.000 0.000 0.000

Plotting the Data

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')