Home

Awesome

A Python Robotics Package QUT Centre for Robotics Open Source

PyPI version Python Version License: MIT

Build Status Coverage PyPI - Downloads GitHub stars

<table style="border:0px"> <tr style="border:0px"> <td style="border:0px"> <img src="https://github.com/petercorke/bdsim/raw/master/figs/BDSimLogo_NoBackgnd@2x.png" width="300"></td> <td style="border:0px"> A Python block diagram simulation package</a> <ul> <li><a href="https://github.com/petercorke/bdsim">GitHub repository </a></li> <li><a href="https://petercorke.github.io/bdsim">Documentation</a></li> <li><a href="https://github.com/petercorke/bdsim/wiki">Wiki (examples and details)</a></li> <li><a href="installation#">Installation</a></li> <li>Dependencies: numpy, scipy, matplotlib, <a href="https://github.com/petercorke/ansitable">ansitable</a>, ffmpeg (if rendering animations as a movie)</li> </ul> </td> </tr> </table>

bdsim is Python 3 package that enables modelling and simulation of continuous-time, discrete-time or hybrid dynamic systems. Systems are conceptualized in block diagram form, but represented in terms of Python objects.

<table> <tr> <td> <img width=450 src="https://github.com/petercorke/bdsim/raw/master/figs/bd1-sketch.png"> </td> <td style="padding-left: 20px;"> <pre style="font-size:10px;"> # define the blocks demand = bd.STEP(T=1, name='demand') sum = bd.SUM('+-') gain = bd.GAIN(10) plant = bd.LTI_SISO(0.5, [2, 1]) scope = bd.SCOPE(styles=['k', 'r--']) # connect the blocks bd.connect(demand, sum[0], scope[1]) bd.connect(plant, sum[1]) bd.connect(sum, gain) bd.connect(gain, plant) bd.connect(plant, scope[0]) </pre> </td> </tr> </table>

Key features include:

Getting started

We first sketch the dynamic system we want to simulate as a block diagram, for example this simple first-order system

block diagram

which we can express concisely with bdsim as (see bdsim/examples/eg1.py)

     1  #!/usr/bin/env python3
     2  import bdsim	
     4  sim = bdsim.BDSim()  # create simulator
     5  bd = sim.blockdiagram()  # create an empty block diagram
     6	
     7	# define the blocks
     8	demand = bd.STEP(T=1, name='demand')
     9	sum = bd.SUM('+-')
    10	gain = bd.GAIN(10)
    11	plant = bd.LTI_SISO(0.5, [2, 1], name='plant')
    12	scope = bd.SCOPE(styles=['k', 'r--'])
    13	
    14	# connect the blocks
    15	bd.connect(demand, sum[0], scope[1])
    17	bd.connect(sum, gain)
    18	bd.connect(gain, plant)
    19	bd.connect(plant, sum[1], scope[0])
    20	
    21	bd.compile()          # check the diagram
    22
    23	sim.report(bd)   # list the system
    24  out = sim.run(bd, 5)   # simulate for 5s

which is just 15 lines of executable code.

The red block annotations on the hand-drawn diagram are used as the names of the variables holding references to the block instance. The blocks can also have user-assigned names, see lines 8 and 11, which are used in diagnostics and as labels in plots.

After the blocks are created their input and output ports need to be connected. In bdsim all wires are point to point, a one-to-many connection is implemented by many wires, for example

bd.connect(source, dest1, dest2, ...)

creates individual wires from source -> dest1, source -> dest2 and so on. Ports are designated using Python indexing notation, for example block[2] is port 2 (the third port) of block. Whether it is an input or output port depends on context. In the example above an index on the first argument refers to an output port, while on the second (or subsequent) arguments it refers to an input port. If a block has only a single input or output port then no index is required, 0 is assumed.

A group of ports can be denoted using slice notation, for example

bd.connect(source[2:5], dest[3:6)

will connect source[2] -> dest[3], source[3] -> dest[4], source[4] -> dest[5]. The number of wires in each slice must be consistent. You could even do a cross over by connecting source[2:5] to dest[6:3:-1].

Line 21 assembles all the blocks and wires, instantiates subsystems, checks connectivity to create a flat wire list, and then builds the dataflow execution plan.

Line 23 generates a report, in tabular form, showing a summary of the block diagram:

┌────────┬──────────┬────────┬────────┬─────────────┐
│ block  │   type   │ inport │ source │ source type │
├────────┼──────────┼────────┼────────┼─────────────┤
│demand@ │ step     │        │        │             │
├────────┼──────────┼────────┼────────┼─────────────┤
│gain.0  │ gain     │ 0      │ sum.0  │ float64     │
├────────┼──────────┼────────┼────────┼─────────────┤
│plant   │ lti_siso │ 0      │ gain.0 │ float64     │
├────────┼──────────┼────────┼────────┼─────────────┤
│scope.0 │ scope    │ 0      │ plant  │ float64     │
│        │          │ 1      │ demand │ int         │
├────────┼──────────┼────────┼────────┼─────────────┤
│sum.0   │ sum      │ 0      │ demand │ int         │
│        │          │ 1      │ plant  │ float64     │
└────────┴──────────┴────────┴────────┴─────────────┘

Line 24 runs the simulation for 5 seconds using the default variable-step RK45 solver and saves output values at least every 0.05s. It causes the following output

>>> Start simulation: T = 5.00, dt = 0.050
  Continuous state variables: 1
     x0 =  [0.]
  Discrete state variables:   0

no graphics backend specified: Qt5Agg found, using instead of MacOSX

bdsim ◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉ 100.0% - 0s

<<< Simulation complete
  block diagram evaluations: 784
  block diagram exec time:   0.075 ms
  time steps:                123
  integration intervals:     2

This provides a summary of the number of states for the complete system: the number of continuous states, the number of discrete states, and the initial value of the state vectors.

During execution a progress bar is updated and scope blocks pops up a graphical window

bdsim output

The simulation results are in a container object (BDStruct)

>>> out
t      = ndarray:float64 (123,)
x      = ndarray:float64 (123, 1)
xnames = ['plantx0'] (list)
ynames = [] (list)            

which contains an array of time values, an array of state values, and a list of the names of the state variables.

By default the .run() method at line 24 blocks blocks the script until all figure windows are closed (by pressing the operating system close button or typing "q"), or the script is killed with SIGINT. If you want to continue the script with the figures still active then the hold=False option should be set.

If we wished to also record additional outputs, we can add them as watched signals

out = sim.run(bd, watch=[demand, sum])  # simulate for 5s

and now the output is

>>> out
t      = ndarray:float64 (123,)
x      = ndarray:float64 (123, 1)
xnames = ['plantx0'] (list)
y0     = ndarray:float64 (123,)
y1     = ndarray:int64 (123,)
ynames = ['plant[0]', 'demand[0]'] (list)

where

The watch argument is a list of outputs to log, in this case plant defaults to output port 0. This information is saved in additional variables y0, y1 etc. ynames is a list of the names of the watched variables.

An alternative system report, created by sim.report(bd, type="lists") is more detailed

Blocks::

┌───┬─────────┬─────┬──────┬────────┬─────────┬───────┐
│id │    name │ nin │ nout │ nstate │ ndstate │ type  │
├───┼─────────┼─────┼──────┼────────┼─────────┼───────┤
│ 0 │  demand │   0 │    1 │      0 │       0 │ step  │
│ 1 │   sum.0 │   2 │    1 │      0 │       0 │ sum   │
│ 2 │  gain.0 │   1 │    1 │      0 │       0 │ gain  │
│ 3 │   plant │   1 │    1 │      1 │       0 │ LTI   │
│ 4 │ scope.0 │   2 │    0 │      0 │       0 │ scope │
└───┴─────────┴─────┴──────┴────────┴─────────┴───────┘

Wires::

┌───┬──────┬──────┬──────────────────────────┬─────────┐
│id │ from │  to  │       description        │  type   │
├───┼──────┼──────┼──────────────────────────┼─────────┤
│ 0 │ 0[0] │ 1[0] │ demand[0] --> sum.0[0]   │ int     │
│ 1 │ 0[0] │ 4[1] │ demand[0] --> scope.0[1] │ int     │
│ 2 │ 3[0] │ 1[1] │ plant[0] --> sum.0[1]    │ float64 │
│ 3 │ 1[0] │ 2[0] │ sum.0[0] --> gain.0[0]   │ float64 │
│ 4 │ 2[0] │ 3[0] │ gain.0[0] --> plant[0]   │ float64 │
│ 5 │ 3[0] │ 4[0] │ plant[0] --> scope.0[0]  │ float64 │
└───┴──────┴──────┴──────────────────────────┴─────────┘

In the first table we can see key information about each block, its id (used internally), name, the number of input and output ports, the number of continuous- and discrete-time states, and the type which is the block class. Note that the name is auto-generated based on the type, except if it has been set explicitly as for the blocks demand and plant.

The second table shows all wires in point-to-point form, showing the start and end block and port (the block is represented here by its id) and the type of the object sent along the wire.

To save figures we need to make two modifications, changing line 4 to

     4  sim = bdsim.BDSim(hold=False)  # create simulator

which prevents .run() from blocking and then deleting all the figures. Then, after the .run() we add

     25 scope.savefig()  # save scope figure

If the filename is not given it defaults to the block name, in this case scope.0.pdf.

The output can be pickled and written to a file

examples/eg1.py -o
python -mpickle bd.out
t      = ndarray:float64 (123,)
x      = ndarray:float64 (123, 1)
xnames = ['plantx0'] (list)
y0     = ndarray:float64 (123,)
y1     = ndarray:int64 (123,)
ynames = ['plant[0]', 'demand[0]'] (list)

by default the results are written to bd.out, use the option --out FILE to set it to a specific value.

The block parameters can also be overridden from the command line without having to edit the code. To increase the loop gain we could write:

examples/eg1.py --set gain.0:K=20

More details on this Wiki about:

Other examples

In the folder bdsim/examples you can find a few other runnable examples:

Examples from Chapter four of Robotics, Vision & Control 2e (2017):

RVC Figure 4.4

RVC Figure 4.6

RVC Figure 4.6

RVC Figure 4.11

Figs 4.8 (pure pursuit) and Fig 4.21 (quadrotor control) are yet to be done.

A more concise way

Wiring, and some simple arithmetic blocks like GAIN, SUM and PROD can be implicitly generated by overloaded Python operators. This strikes a nice balance between block diagram coding and Pythonic programming.

     1  #!/usr/bin/env python3
     2
     3  import bdsim
     4
     5  sim = bdsim.BDSim()  # create simulator
     6  bd = sim.blockdiagram()  # create an empty block diagram
     7
     8  # define the blocks
     9  demand = bd.STEP(T=1, name='demand')
    10  plant = bd.LTI_SISO(0.5, [2, 1], name='plant')
    11  scope = bd.SCOPE(styles=['k', 'r--'], movie='eg1.mp4')
    12
    13  # connect the blocks using Python syntax
    14  scope[0] = plant
    15  scope[1] = demand
    16  plant[0] = 10 * (demand - plant)
    17
    18  bd.compile()   # check the diagram
    19  bd.report()    # list all blocks and wires
    20
    22  out = sim.run(bd, 5, watch=[plant,])  # simulate for 5s

This requires fewer lines of code and the code is more readable. Importantly, it results in in exactly the same block diagram in terms of blocks and wires

Wires::

┌───┬──────┬──────┬──────────────────────────────┬─────────┐
│id │ from │  to  │         description          │  type   │
├───┼──────┼──────┼──────────────────────────────┼─────────┤
│ 0 │ 1[0] │ 2[0] │ plant[0] --> scope.0[0]      │ float64 │
│ 1 │ 0[0] │ 2[1] │ demand[0] --> scope.0[1]     │ int     │
│ 2 │ 0[0] │ 3[0] │ demand[0] --> _sum.0[0]      │ int     │
│ 3 │ 1[0] │ 3[1] │ plant[0] --> _sum.0[1]       │ float64 │
│ 4 │ 3[0] │ 4[0] │ _sum.0[0] --> _gain.0(10)[0] │ float64 │
│ 5 │ 4[0] │ 1[0] │ _gain.0(10)[0] --> plant[0]  │ float64 │
└───┴──────┴──────┴──────────────────────────────┴─────────┘

The implicitly created blocks have names prefixed with an underscore.

bdedit: the graphical editing tool

block diagram

bdedit is a multi-platform PyQt5-based graphical tool to create, edit, render and execute block diagram models.

From the examples folder

% bdedit eg1.bd

will create a display like that shown above. Pushing the run button, top left (triangle in circle) will spawn bdrun as a subprocess which will:

<a href="https://www.youtube.com/watch?v=1qM5D0jekjc"><img width=100 style="vertical-align:middle" src="https://cdn.mos.cms.futurecdn.net/8gzcr6RpGStvZFA2qRt4v6-1920-80.jpg.webp"> <span style="">Screencast showing bdedit usage.</span></a>

Article

I published this article on LinkedIn, which describes the thought process behind bdsim.

Limitations

There are lots! The biggest is that bdsim is based on a very standard variable-step integrator from the scipy library. For discontinuous inputs (step, square wave, triangle wave, piecewise constant) the transitions get missed. This also makes it inaccurate to simulate hybrid discrete-continuous time systems. We really need a better integrator, perhaps odedc from SciLab could be integrated.