Skip to content

Creating a new block

Peter Corke edited this page Apr 18, 2023 · 9 revisions

WORK IN PROGRESS

Basics of blocks

What is a block

Blocks are defined by class definitions in modules in the bdsim/blocks folder or the blocks folder of a compatible toolbox.

The class defining the block must subclass one of:

  • SourceBlock, output is a constant or function of time. For example, CONSTANT blocks and WAVEFORM generator blocks
  • SinkBlock, input only. For example, PRINT blocks or NULL blocks.
  • GraphicsBlock, a subclass of Sink for blocks that produce graphical output. For example SCOPE blocks.
  • FunctionBlock, output is a direct function of input. For example GAIN blocks or FUNCTION blocks.
  • TransferBlock, output is a function of state self.x (no direct pass through). For example LTI_SISO blocks.

which are imported by bdsim.components. These classes all subclass Block.

All block classes have names starting with a capital letter, and if long can use camel case, eg. the GAIN block is a defined by a class called Gain.

Finding and loading block definitions

When the BDSim instance is constructed

import bdsim

sim = bdsim.BDSim()  # create simulator

it searches all modules on the block search path. Any class found which is a subclass of Block is added to a list which can be displayed by

sim.blocks()

This list includes metadata about where the block definition was found, a reference to its constructor (__init__ method), and its doctoring.

When the BlockDiagram object is constructed

bd = sim.blockdiagram()  # create an empty block diagram

the __init__ method of every block class becomes a factory method of the BlockDiagram instance.
The factory method name is derived from the class name: leading underscores are stripped and the name is capitalized, eg. the class Gain becomes the factory method GAIN.

Any docstring for the original block class (class or __init__) becomes a docstring of the factory method.

Block methods

Every class must provide several methods:

  • __init__ mandatory to handle block specific parameter arguments. Additional common block parameters are handled by the superclasses.
  • start, to setup just before simulation starts
  • output, to compute the output value as a function of self.inputs which is a dict indexed by input number
  • deriv, for Transfer subclass only, return the state derivative vector
  • check, to validate parameter settings

During block diagram network evaluation the blocks are executed in order according to a schedule which ensures that their inputs have been computed by other blocks and are available as self.input(i) where i is the input port number, or as a list self.inputs ordered by input port number.

blockargs, optional block parameters

Common to all blocks and handled by the superclass constructor include:

  • name the block name
  • onames a tuple of names for output ports, can be mathtext format
  • inames a tuple of names for input ports, can be mathtext format
  • snamesa tuple of names for states, can be mathtext format
  • pos the position of the block in a graphical representation

Writing your own block

The first important decision is to decide what type of block you are creating, as per the list above.

A good way to create a new block is to start with an existing block that is somewhat close to the new block you want to create. You can find examples of many blocks, organised by their type in the files in the bdsim/blocks folder, for example bdsim/blocks/functions.py contains all the FunctionBlock subclasses like GAIN, SUM etc.

Block classes have camel-case names and start with a capital letter, eg. Waveform. Exceptions are acronyms which are all capital letters, eg. LTI_SISO. Each block class must provide a number of methods:

  • __init__ invoked when the block is created. Invoking bd.WAVEFORM(...) invokes the constructor __init__(...) of the Waveform class.
  • update
  • deriv

Define the class variables

class MyBlock(FunctionBlock):

    nin = 1
    nout = 1

which declares to bdsim the number of input and output ports the block has. If they are not known, or are variable, set the corresponding value to -1. These are provided to inform bdedit about a block without having to instantiate it.

Create the __init__ method

Use a signature line like

    def __init__(self, ..., **blockargs):
        super().__init__(**blockargs)

where the ellipsis represents your block's parameters. blockargs are standard parameters that are handled by the superclass constructor. These parameters include nin and nout which can be used to dynamically set the number of block inputs and outputs.

This method should do the following:

  • validate the passed parameters
  • save those that are required for execution time as attributes of the object. Do not use attribute names with underscore prefixes since that namespace is reserved for bdsim. You may also wish to stash other data at this time

Create the working part of your block

The method you need to create will depend on the type of block you are creating. For a:

  • SourceBlock or FunctionBlock it will be called output
  • SinkBlock it will be called step
  • TranferBlock you will need to create an output method and a deriv method

output method

For SourceBlock, FunctionBlock, or TranferBlock compute the block's output port values $y = f(x, u, t)$ where $x$ is the state (TransferBlock only), $u$ is the input, and $t$ is time.

    def output(self, t, inports, x):
  • The current time is t as a float
  • The inputs to the block are available in the list inputs
  • The state vector of this block is available as x which is a 1D Numpy array.
  • Block parameters are available as attributes of self created by the __init__ method

The method must return a list of output port values. For a single-output block it must return a list with a single element.

step method

For SinkBlock or GraphicsBlock only, since it has no outputs.

    def step(self, t, inports):

        # body in here

        super().step(t, inports)    # last line
  • The current time is t as a float
  • The inputs to the block are available in the list inputs
  • Block parameters are available as attributes of self created by the __init__ method

A block of this type has no return value. It typically takes the input and saves it or transmit in some fashion.

The last line should call the superclass method, for a graphics block this will update graphics, save animation frames etc.

deriv method

For TransferBlock only. The dynamics of the block are described generally by $\dot{x} = f(x, u, t)$ where $x$ is the state, $u$ is the input, and $t$ is time.

    def deriv(self, t, inports, x):
  • The current time is t as a float
  • The inputs to the block are available in the list inputs
  • The state vector of this block is available as x which is a 1D Numpy array.
  • Block parameters are available as attributes of self created by the __init__ method

The method must return a 1D Numpy array of the same length as x.

next method

For discrete-time TransferBlock only. The dynamics of the block are described generally by $x_{k+1} = f(x_k, u, t)$ where $x$ is the state, $u$ is the input, and $t$ is time.

    def next(self, t, inports, x):
  • The current time is t as a float
  • The inputs to the block are available in the list inputs
  • The state vector of this block is available as x which is a 1D Numpy array.
  • Block parameters are available as attributes of self created by the __init__ method

The method must return a 1D Numpy array of the same length as x.

start method

    def start(self, simstate):
        super().start(simstate)

        # your code here

The method is passed the runtime state which contains a grab bag of useful references regarding the execution environment of the diagram.

This method is called before simulation commences and generally doesn't need to be implemented.
The exception is for graphics blocks that need to get access to the graphic subsystem to create a plot window. Command line options are available in simstate.options and the maximum simulation time is simstate.T.

If other methods need simstate, which is pretty rare, then the required references should be stashed as a block instance variable in this method, so that it can be accessed by other methods.

Finishing touches

Create an icon for bdedit

See the documentation in the blocks/Icons folder.

Create unit tests

Unit tests are important. Have a look at the tests in the tests folder. You can instantiatiate your block and use the T_xxx methods to evaluate its performance without having to create a block diagram or runtime.

Add docstrings

Docstrings are important for several reasons:

  • let the code reader know what the block is doing
  • provide hinting inside the IDE.
  • provide key data structures to bdedit which it uses for input validation

The docstrings should be written in the general format of docstrings for existing blocks. There is a docstring for the class, and one for the __init__ method. For the latter, each parameter should be described in Sphinx format with roles :param: and a :type:. Some specific syntax used by bdedit is available here.

Clone this wiki locally