Overview

Getting started

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

_images/bd1-sketch.png

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

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

This example utilizes just 16 lines of executable code. The red block annotations on the hand-drawn diagram correspond to the variable names holding references to the block instances.

  • Lines 7-11 defines the blocks used in the model. Blocks can have user-assigned names (see lines 7 and 10), which are useful for diagnostics and plot labels.

  • Lines 14-17 connects the blocks together. In bdsim, all wires are point-to-point, so a one-to-many connection is implemented by defining multiple wires. The first argument to connect is the source block, and subsequent arguments are destination blocks.

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

    Ports are designated using Python indexing notation; for instance, block[2] refers to the third port of the block. Context determines if a port is an input or output: an index on the first argument of connect refers to an output, while subsequent arguments refer to inputs. If a block has only one port, the index 0 is assumed.

    Groups of ports can be denoted using slice notation, for example:

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

    connects source[2] to dest[3], source[3] to dest[4], and source[4] to dest[5].

  • Line 19 assembles the blocks, checks connectivity to create a flat wire list, and builds the dataflow execution plan.

  • Line 20 generates a tabular report summarizing the diagram’s blocks and wires.

    ┌─────────┬────┬────┬──────────┬────────┬────────┬─────────────┐
    │  block  │ nc │ nd │   type   │ inport │ source │ source type │
    ├─────────┼────┼────┼──────────┼────────┼────────┼─────────────┤
    │ demand@ │ 0  │ 0  │ step     │        │        │             │
    ├─────────┼────┼────┼──────────┼────────┼────────┼─────────────┤
    │ gain.0  │ 0  │ 0  │ gain     │ 0      │ sum.0  │ float64     │
    ├─────────┼────┼────┼──────────┼────────┼────────┼─────────────┤
    │ plant   │ 1  │ 0  │ lti_siso │ 0      │ gain.0 │ float64     │
    ├─────────┼────┼────┼──────────┼────────┼────────┼─────────────┤
    │ scope.0 │ 0  │ 0  │ scope    │ 0      │ plant  │ float64     │
    │         │    │    │          │ 1      │ demand │ int         │
    ├─────────┼────┼────┼──────────┼────────┼────────┼─────────────┤
    │ sum.0   │ 0  │ 0  │ sum      │ 0      │ demand │ int         │
    │         │    │    │          │ 1      │ plant  │ float64     │
    └─────────┴────┴────┴──────────┴────────┴────────┴─────────────┘
    
  • Line 22 executes the simulation for the specified duration (5 seconds).

To record a movie of a graphics block instead of (or as well as) displaying it live, pass movie='filename.mp4' to that block’s constructor (requires ffmpeg); to keep figure windows open and interactive after the run finishes, call sim.done(bd, block=True) — both shown in the next example below.

The simulation results are returned in a simple container object (see the wiki’s Simulation results page for the full picture, including discrete/clocked state):

>>> print(out)
t      = ndarray:float64 (123,)
x      = ndarray:float64 (123, 1)
xnames = ['plant:x_0'] (list)

To record additional simulation variables, use a WATCH block or the watch option in run:

>>> out = sim.run(bd, 5, watch=[plant, demand])
>>> print(out)
t      = ndarray:float64 (123,)
x      = ndarray:float64 (123, 1)
xnames = ['plant:x_0'] (list)
y      = ndarray:float64 (123, 2)
ynames = ["Block.continuous('plant', type=lti_siso, nin=1, nout=1, nstates=1)[0]",
          "Block.source('demand', type=step, nout=1)[0]"] (list)
>>> plt.plot(out.t, out.y[:,0], 'k', out.t, out.y[:,1], 'r--')

ynames defaults to each watched block/plug’s full repr() as shown above; it’s usually clearer in practice, since demand and plant already have names here.

Using operator overloading

Wiring and arithmetic blocks like GAIN, SUM, and PROD explicitly is somewhat tedious. An alternative is to implicitly generate and wire arithmetic blocks (GAIN, SUM, PROD) using overloaded Python operators, striking a balance between block diagram logic and Pythonic programming:

 1import bdsim
 2
 3sim = bdsim.BDSim()
 4bd = sim.blockdiagram()
 5
 6# define the blocks
 7demand = bd.STEP(T=1, name='demand')
 8plant = bd.LTI_SISO(0.5, [2, 1], name='plant')
 9scope = bd.SCOPE(styles=['k', 'r--'], movie='eg1.mp4')
10
11# connect the blocks
12scope[0] = plant
13scope[1] = demand
14plant[0] = 10 * (demand - plant)
15
16bd.compile()
17bd.report_summary()
18
19out = sim.run(bd, 5)
20print(out)
21
22sim.done(bd, block=True)

This approach requires fewer lines of code and is arguably is more readable, while producing exactly the same underlying block diagram. Implicitly created blocks are automatically named with an underscore prefix.

A list of available blocks can be obtained by:

>>> sim.blocks()
89  blocks loaded
bdsim.blocks.connections................: ITEM DICT MUX DEMUX INDEX SUBSYSTEM INPORT OUTPORT
bdsim.blocks.continuous.................: INTEGRATOR LTI_SS LTI_SISO DERIV2 DERIV PID
bdsim.blocks.displays...................: SCOPE SCOPEXY SCOPEXY1 ANIMATION
bdsim.blocks.functions..................: SUM PROD GAIN POW CLIP FUNCTION INTERPOLATE
bdsim.blocks.linalg.....................: INVERSE TRANSPOSE NORM FLATTEN SLICE2 SLICE1 DET COND
bdsim.blocks.sampled....................: ZOH INTEGRATOR_S DERIV_S LTI_SS_S LTI_SISO_S PID_S DINTEGRATOR
bdsim.blocks.sinks......................: PRINT STOP EVENT NULL WATCH
bdsim.blocks.sources....................: CONSTANT TIME WAVEFORM PIECEWISE STEP RAMP
bdsim.blocks.spatial....................: POSE_POSTMUL POSE_PREMUL TRANSFORM_VECTOR POSE_INVERSE POSEINTEGRATOR
........................................: DPOSEINTEGRATOR
roboticstoolbox.blocks.arm..............: FKINE IKINE JACOBIAN ARMPLOT JTRAJ CTRAJ CIRCLEPATH TRAPEZOIDAL TRAJ IDYN
........................................: GRAVLOAD_X INERTIA INERTIA_X FDYN FDYN_X
roboticstoolbox.blocks.mobile...........: BICYCLE UNICYCLE DIFFSTEER VEHICLEPLOT
roboticstoolbox.blocks.spatial..........: TR2DELTA DELTA2TR POINT2TR TR2T
roboticstoolbox.blocks.uav..............: MULTIROTOR MULTIROTORMIXER MULTIROTORPLOT
machinevisiontoolbox.blocks.camera......: CAMERA VISJAC_P ESTPOSE_P IMAGEPLANE

(the hardware I/O blocks, bdsim.blocks.io, live on the unmerged feat/realtime branch, not main — see the wiki’s Real-time overview)

More details can be found at: