Package: bdsim
A Python block diagram simulation packageGitHub repositoryDocumentationWiki (examples and details)InstallationDependencies: numpy, scipy, matplotlib,ansitable, ffmpeg (if rendering animations as a movie)bdsimis 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.# 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])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 communicateanyPython type such as scalars, lists, dicts, NumPy arrays, objects, and functions. For robotics and vision applications using theSpatial Maths Toolbox for Pythonwires could send values such asSE3,UnitQuaternionorTwist3objects.Over 70 blocks for linear, nonlinear functions, display blocks, as well as continuous- and discrete-time dynamicsEasy to add your own block, it's simply a classSubsystems 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 theRobotics Toolbox for Python(such as arm, ground and aerial robots) andMachine Vision Toolbox for Python(such as cameras). These are defined in theblocksfolder of those toolboxes.The diagram can be executed in a headless configuration, particularly useful on an embedded computer like a RaspberryPi.Apython-based graphical editorallows graphical creation of block diagramsthe diagram is stored in a human readable/editable JSON file with extension.bdcreates good-quality graphics for inclusion in publicationscan launchbdsimto import and execute the modelautomatically discovers all bsdim and toolbbox blocks and adds them to the block library menuicons can be easily created using any image creation tool or a LaTeX expressionGetting startedWe first sketch the dynamic system we want to simulate as a block diagram, for example this simple first-order systemwhich we can express concisely withbdsimas (seebdsim/examples/eg1.py)1#!/usr/bin/env python32importbdsim4sim=bdsim.BDSim()# create simulator5bd=sim.blockdiagram()# create an empty block diagram67# define the blocks8demand=bd.STEP(T=1,name='demand')9sum=bd.SUM('+-')10gain=bd.GAIN(10)11plant=bd.LTI_SISO(0.5,[2,1],name='plant')12scope=bd.SCOPE(styles=['k','r--'])1314# connect the blocks15bd.connect(demand,sum[0],scope[1])17bd.connect(sum,gain)18bd.connect(gain,plant)19bd.connect(plant,sum[1],scope[0])2021bd.compile()# check the diagram2223sim.report(bd)# list the system24out=sim.run(bd,5)# simulate for 5swhich 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. Inbdsimall wires are point to point, aone-to-manyconnection is implemented bymanywires,
for examplebd.connect(source, dest1, dest2, ...)creates individual wires fromsource->dest1,source->dest2and so on.
Ports are designated using Python indexing notation, for exampleblock[2]is port 2 (the third port) ofblock.  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 examplebd.connect(source[2:5], dest[3:6)will connectsource[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 connectingsource[2:5]todest[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:     2This 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 windowThe 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 thehold=Falseoption should be set.If we wished to also record additional outputs, we can add them aswatchedsignalsout = sim.run(bd, watch=[demand, sum])  # simulate for 5sand 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)wheretthe time vector: ndarray, shape=(M,)xis the state vector: ndarray, shape=(M,N), one row per timestepxnamesis a list of the names of the states corresponding to columns ofx, eg. "plant.x0"Thewatchargument is a list of outputs to log, in this caseplantdefaults
to output port 0.  This information is saved in additional variablesy0,y1etc.ynamesis a list of the names of the watched variables.An alternative system report, created bysim.report(bd, type="lists")is more detailedBlocks::

┌───┬─────────┬─────┬──────┬────────┬─────────┬───────┐
│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, itsid(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 blocksdemandandplant.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 itsid) and the type of the object sent along the wire.To save figures we need to make two modifications, changing line 4 to4  sim = bdsim.BDSim(hold=False)  # create simulatorwhich prevents.run()from blocking and then deleting all the figures.
Then, after the.run()we add25 scope.savefig()  # save scope figureIf the filename is not given it defaults to the block name, in this casescope.0.pdf.The output can be pickled and written to a fileexamples/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 tobd.out, use the option--out FILEto 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=20More details on this Wiki about:Adding blocksConnecting blocksRunning the simulationCommand line optionsOther examplesIn the folderbdsim/examplesyou can find a few other runnable examples:eg1.pythe example given abovewaveform.pytwo signal generators connected to two scopesExamples from Chapter four ofRobotics, Vision & Control 2e (2017):rvc4_2.pyFig 4.2 - a car-like vehicle with bicycle kinematics driven by a rectangular pulse steering signalrvc4_4.pyFig 4.4 - a car-like vehicle driving to a pointrvc4_6.pyFig 4.6 - a car-like vehicle driving to/along a linervc4_8.pyFig 4.8 - a car-like vehicle using pure-pursuit trajectory followingrvc4_11.pyFig 4.11 a car-like vehicle driving to a poseFigs 4.8 (pure pursuit) and Fig 4.21 (quadrotor control) are yet to be done.A more concise wayWiring, and some simple arithmetic blocks likeGAIN,SUMandPRODcan 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 5sThis requires fewer lines of code and the code is more readable. Importantly, it results in inexactly the sameblock diagram in terms of blocks and wiresWires::

┌───┬──────┬──────┬──────────────────────────────┬─────────┐
│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 toolbdeditis a multi-platform PyQt5-based graphical tool to create, edit, render and execute block diagram models.From the examples folder% bdedit eg1.bdwill create a display like that shown above.  Pushing the run button, top left (triangle in circle) will spawnbdrunas a subprocess which will:parse the JSON fileinstantiate all blocks and wirescompile and run the diagramArticleI publishedthis article on LinkedIn, which describes the thought process behind bdsim.LimitationsThere are lots!  The biggest is thatbdsimis 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, perhapsodedcfrom SciLab could be integrated.