-
-
Notifications
You must be signed in to change notification settings - Fork 39
Connecting blocks
We will consider a more complex example (from Robotics Toolbox for Python)
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
FUNCTIONblocks are passedlambdafunctions. The value at the input port is assigned to the parameterdand 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
SUMhas the optionmode='c'to indicate that the signals are angles and to wrap the result into the range [-π π). - the
VEHICLEPLOTblock is passed a function which initialises the graphic display, in this case by marking the start and goal positions. The first argumentscale=[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. -
BICYCLEhas 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 — sobike[2]is invalid here. TheSLICE1blocksxyandthetapull out the sub-vectors we need from that single pose output.
There are many different ways (maybe too many) to express the connections between the blocks.
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.
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.
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.
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
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.
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)
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).
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.
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.
Copyright (c) Peter Corke 2020-
- Home
- API reference (Sphinx)
- Block catalog
- Control Systems Magazine article
- Adding blocks to your model
- Block path
- Connecting blocks
- Compiling
- Running
- Watching a simulation variable
- Simulation results
- Runtime options
- Environment variables
- Discrete-time blocks
- Subsystems
- Figures
- Notebook animation
- Animation and movies
- PID control
- Coding patterns
- Block methods and attributes
- Time stepping: integration, animation & events
- Blocks, wires and plugs
- Graphics blocks
- Evaluation
- Runtimes and simulator state
- Creating a new block
- Related packages
Under development on feat/realtime branch, planned for release before end of 2026.
