Bdsim
Simulate dynamic systems expressed in block diagram form using Python
Install / Use
/learn @petercorke/BdsimREADME
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.
Key features include:
- The block diagram can be created easily using Python code, rather than drawing boxes and wires. This enables use of your favourite IDE, standard version control tools and development workflows.
- Wires can communicate any Python type such as scalars, lists, dicts, NumPy arrays, objects, and functions. For robotics and vision applications using the Spatial Maths Toolbox for Python wires could send values such as
SE3,UnitQuaternionorTwist3objects. - Over 70 blocks for linear, nonlinear functions, display blocks, as well as continuous- and discrete-time dynamics
- Easy to add your own block, it's simply a class
- Subsystems are supported, and a subsystem can be independently instantiated multiple times in a system. Subsystems can also be nested.
- Blocks from other toolboxes are automatically discovered and included. There are blocks for some functions in the Robotics Toolbox for Python (such as arm, ground and aerial robots) and Machine Vision Toolbox for Python (such as cameras). These are defined in the
blocksfolder of those toolboxes.
- The diagram can be executed in a headless configuration, particularly useful on an embedded computer like a RaspberryPi.
- A python-based graphical editor
- allows graphical creation of block diagrams
- the diagram is stored in a human readable/editable JSON file with extension
.bd - creates good-quality graphics for inclusion in publications
- can launch
bdsimto import and execute the model - automatically discovers all bsdim and toolbbox blocks and adds them to the block library menu
- icons can be easily created using any image creation tool or a LaTeX expression
Getting started
We first sketch the dynamic system we want to simulate as a block diagram, for example this simple first-order system

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

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 =
