Skip to content

Connecting blocks

Peter Corke edited this page Aug 19, 2026 · 10 revisions

Connecting blocks

We will consider a more complex example (from Robotics Toolbox for Python)

RVC Figure 4.4

which has been hand annotated with the block names to make the codification process a bit easier.

We start by defining each of the blocks, using the same names as we scribbled on the diagram.

goal = bd.CONSTANT([5, 5])
error = bd.SUM('+-')
d2goal = bd.FUNCTION(lambda d: math.sqrt(d[0]**2 + d[1]**2))
h2goal = bd.FUNCTION(lambda d: math.atan2(d[1], d[0]))
heading_error = bd.SUM('+-', mode='c')
Kv = bd.GAIN(0.5)
Kh = bd.GAIN(4)
bike = bd.BICYCLE(x0=[5, 2, 0])
xy = bd.SLICE1([0, 1])
theta = bd.SLICE1([2])

def background_graphics(ax):
    ax.plot(5, 5, '*')
    ax.plot(5, 2, 'o')

vplot = bd.VEHICLEPLOT(scale=[0, 10], size=0.7, shape='box', init=background_graphics)
vscope = bd.SCOPE(name='velocity')
hscope = bd.SCOPE(name='heading')

Things to note:

  • the FUNCTION blocks are passed lambda functions. The value at the input port is assigned to the parameter d and the function result appears at the output port. In this case, the number of input and output ports are each one (default). A function can receive multiple input arguments, from multiple input ports, and can return results to multiple output ports using a list.
  • the second instance of SUM has the option mode='c' to indicate that the signals are angles and to wrap the result into the range [-π π).
  • the VEHICLEPLOT block is passed a function which initialises the graphic display, in this case by marking the start and goal positions. The first argument scale=[0, 10] indicates the minimum and maximum coordinate, and since it is only a 2-vector it is applied to both the x- and y-axes. A 4-vector allows independent control over the scale for both axes.
  • BICYCLE has a single output port carrying its whole pose (x, y, θ) as one 3-element array (nout=1), not three separate ports. Indexing a block, eg. bike[2], selects a port, not an element within a port's array value — so bike[2] is invalid here. The SLICE1 blocks xy and theta pull out the sub-vectors we need from that single pose output.

Connecting the blocks

There are many different ways (maybe too many) to express the connections between the blocks.

Using connect: point to point

bd.connect(goal, error[0])
bd.connect(bike, xy)
bd.connect(bike, theta)
bd.connect(bike, vplot)
bd.connect(xy, error[1])
bd.connect(error, d2goal)
bd.connect(error, h2goal)
bd.connect(d2goal, Kv)
bd.connect(Kv, bike[0])
bd.connect(Kv, vscope)
bd.connect(h2goal, heading_error[0])
bd.connect(theta, heading_error[1])
bd.connect(heading_error, hscope)
bd.connect(heading_error, Kh)
bd.connect(Kh, bike[1])

which is 15 lines of code.

The first argument is implicitly an output port, and the second and following arguments are implicitly input port.

The arguments can be either blocks or plugs. In the first line, the first argument goal is to a block but the method builds a Plug for goal[0]. If no index given the first port is assumed. The second argument error[0] is a plug.

This format is easy to auto-generate, perhaps from some kind of graphical layout tool.

Using multi-connect

The connect method can accept multiple destinations, ie. connect(src, dest1, dest2, dest3) which creates 3 wires: src→dest1, src→dest2, src→dest3.

bd.connect(goal, error[0])
bd.connect(bike, xy, theta, vplot)     # changed
bd.connect(xy, error[1])
bd.connect(error, d2goal, h2goal)      # changed
bd.connect(d2goal, Kv)
bd.connect(Kv, bike[0], vscope)        # changed
bd.connect(h2goal, heading_error[0])
bd.connect(theta, heading_error[1])
bd.connect(heading_error, hscope, Kh)  # changed
bd.connect(Kh, bike[1])

which has reduced the number of lines of code to express the connections to 10.

Using slices

In the case where multiple wires, on different ports, connect two blocks we can use a more succint notation. Instead of a single port index we can use Python slice notation, with a start value, stop value and optional step. A slice can count upwards, eg. [0:5:2] which is (0, 2, 4)or downwards eg.[5:2:-1]which is(5,4,3)`.

Our running example doesn't have a case for this any more — bike now exposes a single combined output port rather than several matching ports, so xy/theta above are extracted with SLICE1 blocks instead of a port slice. Slice notation remains useful whenever two blocks expose the same number of correspondingly-ordered ports, eg. connecting a DEMUX to a MUX:

demux = bd.DEMUX(3)
mux = bd.MUX(3)
bd.connect(demux[0:3], mux[0:3])

We have used slices to connect multiple ports of demux to the matching ports of mux in one call. All blocks and plugs passed to the connect method must have the same number of wires.

If the source is a slice, then all the destinations must be a slice. A block name by itself is equivalent to block[0] which is a single wire.

Using named ports

bd.connect(goal, error[0])
bd.connect(bike, xy, theta, vplot)
bd.connect(xy, error[1])
bd.connect(error, d2goal, h2goal)
bd.connect(d2goal, Kv)
bd.connect(Kv, bike.v, vscope)        # changed
bd.connect(h2goal, heading_error[0])
bd.connect(theta, heading_error[1])
bd.connect(heading_error, hscope, Kh)
bd.connect(Kh, bike.gamma)            # changed

which is 10 lines of code.

Some blocks have attributes which return a Plug just as indices do. For the BICYCLE block .v is equivalent to [0], and .gamma is equivalent to [1] — these are its two input ports.

These name aliases can be established when you create your own block or as extra arguments to any block. For example, we could rewrite these block definitions as:

goal = bd.CONSTANT([5, 5], onames=('u',))
d2goal = bd.FUNCTION(lambda d: math.sqrt(d[0]**2 + d[1]**2),
    onames=('b',), inames=('a',))

where we pass in tuples of names for the input or output ports. Now we can refer to goal.u or d2goal.a (its input) / d2goal.b (its output). Since unicode characters are allowed in Python identifiers we could use Greek letters for these port names, for example

goal = bd.CONSTANT([5, 5], onames=('α',))
d2goal = bd.FUNCTION(lambda d: math.sqrt(d[0]**2 + d[1]**2),
    onames=('ɣ',), inames=('β',))

and then write

d2goal.β = goal.α

Names can also use matplotlib's mathtext notation which is a simple subset of LaTeX. For example:

goal = bd.CONSTANT([5, 5], onames=(r'$x_\alpha$',))

will create an output port named xalpha where the LaTeX markup characters have been stripped, but the mathtext string would be propogated to other blocks and would be displayed on say the axis or legend of a plot as $x_\alpha$. Note that you need to use a raw string with the r-prefix if the string contains backquotes.

Using assignment

error[0] = goal[0]
xy[0] = bike
theta[0] = bike
vplot[0] = bike
error[1] = xy
d2goal[0] = error
h2goal[0] = error
Kv[0] = d2goal
bike.v = Kv
vscope[0] = Kv
heading_error[0] = h2goal
heading_error[1] = theta
hscope[0] = heading_error
Kh[0] = heading_error
bike.gamma = Kh

which is 15 lines of code. It is not possible to express multi-connections using assignments. Note that the left-hand side must always be a Plug, ie. it must have an index or attribute.

Using implicit connections

We use the >> operator to indicate implicit wiring

error[0] = goal[0]
xy[0] = bike
theta[0] = bike
vplot[0] = bike
error[1] = xy
d2goal[0] = error
h2goal[0] = error
Kv[0] = d2goal
bike.v = Kv
vscope[0] = Kv
heading_error[0] = h2goal
heading_error[1] = theta
hscope[0] = heading_error
bike.gamma = heading_error >> Kh   # changed

which is 14 lines of code. Instead of connecting heading_error to Kh, and then to bike.gamma we have done it implicitly using the >> operator. The value of the right-hand side is Kh. In terms of our previous ways of expressing diagrams this would be

bd.connect(heading_error, Kh)
bd.connect(Kh, bike.gamma)

We could also have chosen to instantiate the Kh block inline by:

bike.gamma = heading_error >> bd.GAIN(4)

Using explicit inputs

All blocks can accept the inputs= argument, a block or plug (or tuple/list of them) that connects to it – given in input port order. For example we could write the summation line from above as

sum = bd.SUM('+-', inputs=(goal, xy))

which says that the inputs to the summing junction are goal (+) and xy (-).

Applying explicit inputs, implicit wiring and named ports we can now write our example, block declaration and wiring, as

bike = bd.BICYCLE(x0=[5, 2, 0])
xy = bd.SLICE1([0, 1], inputs=(bike,), name='xy')
theta = bd.SLICE1([2], inputs=(bike,), name='theta')
error = bd.SUM('+-', inputs=(bd.CONSTANT([5, 5], name='goal'), xy), name='sum')
bike.v = bd.FUNCTION(lambda d: math.sqrt(d[0]**2 + d[1]**2), inputs=(error,), name='d2goal') >> bd.GAIN(0.5, name='Kv')
h2goal = bd.FUNCTION(lambda d: math.atan2(d[1], d[0]), inputs=(error,), name='h2goal')
bike.gamma = bd.SUM('+-', inputs=(h2goal, theta), mode='c', name='hsum') >> bd.GAIN(4, name='Kh')

which is 7 lines of code — xy and theta have to be declared up front here since, unlike the assignment and implicit forms above, inputs= needs an already-existing block or plug to point at.

However we have omitted the scopes, since this compact form doesn't conveniently support one-to-many connections. They're easily added using explicit inputs too, referring back to the theta and bike blocks declared above

bd.SCOPE(name='heading', inputs=(theta,))
bd.VEHICLEPLOT(scale=[0, 10], size=0.7, shape='box', init=background_graphics, inputs=(bike,))

but the velocity signal we want is an intermediate value within the fourth line of the code block above (the output of Kv, chained straight into bike.v). We can use Python's "walrus operator" written as := which is something like C's assignment expression.

To add the velocity scope to the above compact code we could write

bike.v = (velocity := bd.FUNCTION(lambda d: math.sqrt(d[0]**2 + d[1]**2), inputs=(error,), name='d2goal') >> bd.GAIN(0.5, name='Kv'))  # walrus

and then connect a scope to that intermediate value

bd.SCOPE(name='velocity', inputs=(velocity,))

Note that it is not valid to write x = y := a, we must write x = (y := a).

Wiring summary

There are many ways to express your block diagram in code. The first form shown is very verbose but easy to write, and also suitable for auto-generated block diagrams. The final forms are compact and much more like regular programming, with blocks and wires being created under the hood to support the next step of evaluation and simulation.

You can mix and match approaches to suit your own preferences and style.

Errors during wiring

The only errors checked for during the wiring phase are if the variables passed to connect are not block instances, or if a port attribute, eg. block.a, is referenced that does not exisit. Extensive error checking occurs at compilation stage.

Clone this wiki locally