--- 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" ---
{% raw %}
{% endraw %} {% raw %}
%load_ext autoreload
%autoreload 2
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload
{% endraw %}

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.

{% raw %}
{% endraw %} {% raw %}

ControlUnitIndices[source]

Enum = [PER_INDEX, OUT_INDEX, REF_INDEX, ACT_INDEX]

An enumeration.

{% endraw %} {% raw %}
{% endraw %} {% raw %}

class PCTNode[source]

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.

{% endraw %} {% raw %}
{% endraw %} {% raw %}
#node.summary()
{% endraw %} {% raw %}

class PCTNodeData[source]

PCTNodeData(name='pctnodedata')

Data collected for a PCTNode

{% endraw %} {% raw %}
{% endraw %}

Creating a Node

A node can be created simply.

{% raw %}
node = PCTNode()
node.summary()
pctnode PCTNode
----------------------------
REF: constant Constant | 0 
PER: variable Variable | 0 
COM: subtract Subtract | 0 | links  constant variable 
OUT: proportional Proportional | gain 1 | 0 | links  subtract 
----------------------------
{% endraw %}

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.

{% raw %}
dynamic_module_import( 'pct.functions', 'Constant')
{% endraw %} {% raw %}
reference = Constant(1)
namespace=reference.namespace
{% endraw %} {% raw %}
node = PCTNode(name="mypctnode", history=True, reference = reference, output=Proportional(10, namespace=namespace), namespace=namespace)
node.summary()
mypctnode PCTNode
----------------------------
REF: constant Constant | 1 
PER: variable Variable | 0 
COM: subtract Subtract | 0 | links  constant variable 
OUT: proportional Proportional | gain 10 | 0 | links  subtract 
----------------------------
{% endraw %}

Another way of creating a node is by first declaring the functions you want and passing them into the constructor.

{% raw %}
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)
{% endraw %}

Yet another way to create a node is from a text configuration.

{% raw %}
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)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-56-5f01ccd49f82> in <module>
      3     'percoll': {'0': {'type': 'Variable', 'name': 'velocity', 'value': 0.2, 'links': {}}},
      4     'comcoll': {'0': {'type': 'Subtract', 'name': 'subtract', 'value': 1, 'links': {0: 'constant', 1: 'velocity'}}},
----> 5     'outcoll': {'0': {'type': 'Proportional', 'name': 'proportional', 'value': 10, 'links': {0: 'subtract'}, 'gain': 10}}}, namespace=namespace)

<ipython-input-48-cd0de577147b> in from_config(cls, config, namespace)
    466         collection = node.referenceCollection
    467         coll_dict = config['refcoll']
--> 468         PCTNode.collection_from_config(collection, coll_dict, namespace)
    469 
    470         node.perceptionCollection = []

<ipython-input-48-cd0de577147b> in collection_from_config(node, collection, coll_dict, namespace)
    497             #print(fndict)
    498             #func = eval(fnname).from_config(fndict, namespace)
--> 499             func = FunctionFactory.createFunctionFromConfig(fnname, namespace, fndict)
    500 
    501             collection.append(func)

/mnt/c/Users/ryoung/Versioning/python/nbdev/pct/pct/functions.py in createFunctionFromConfig(id, namespace, config)
    410             FunctionFactory.factories[id] = \
    411               eval(id + f'.FactoryFromConfig()')
--> 412         return FunctionFactory.factories[id].create(new_name=False, namespace=namespace, **config)
    413     createFunctionFromConfig = staticmethod(createFunctionFromConfig)
    414 

TypeError: create() got an unexpected keyword argument 'new_name'
{% endraw %}

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.

{% raw %}
integratingnode.summary()
{% endraw %} {% raw %}
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()
{% endraw %}

A node can also be viewed graphically as a network of connected nodes.

{% raw %}
import os
if os.name=='nt':
    integratingnode.draw(node_size=2000, figsize=(8,4))
{% endraw %}

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.

{% raw %}
def velocity_model(velocity,  force , mass):
    velocity = velocity + force / mass
    return velocity

mass = 50
force = 0
{% endraw %}

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.

{% raw %}
velocity=0
force = node()
velocity = velocity_model(velocity, force, mass)
node.set_perception_value(velocity)
print(force)
assert force == 10
{% endraw %}

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.

{% raw %}
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)
    
{% endraw %}

Save and Load

Save a node to file.

{% raw %}
import json
integratingnode.save("inode.json")
{% endraw %}

Create a node from file.

{% raw %}
nnode = PCTNode.load("inode.json")
print(nnode.get_config())
{% endraw %}

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.

{% raw %}
from IPython.display import Image
Image(url='http://www.perceptualrobots.com/wp-content/uploads/2020/08/pct_node_plot.png') 
{% endraw %} {% raw %}
#from nbdev import *
#notebook2script()
{% endraw %}