Towards more complexity

In this second page of the tutorials, we’re going to cover topics that will help you customize your scripts and write them more efficiently. The tools and concepts presented here are really not that advanced or complicated, and will be needed by anyone who wants to write scripts with a minimum of complexity. So, make sure to read this page until the end !

1. Using feedback loops

In the previous tutorials page, we only used linear data flow patterns. Here, we’re going to introduce the concept of feedback loops in a script. The main idea is that although Links are unidirectional, it is totally possible to have them form a loop to send back information to a Block. This is especially useful for driving Generator Blocks, as detailed in a next section. For now, let’s look at the example script given in the tutorial section dedicated to the Machine Block. The Fake Machine Actuator that is used takes its commands as a voltage, which is quite unsatisfying since the achieved speed will vary depending on the characteristics of the motor. Instead, it would be preferable to send speed commands, and to somehow have the motor adapt and reach this speed.

To achieve this behavior, a possibility is to use the PID Block. It will receive on the one hand the target speed, and on the other hand the current speed of the motor. Based on these inputs, it will generate a voltage command to send to the Machine Block driving the Fake Motor. If the PID is well set, the measured speed should reach the target value after some time ! Here’s how the Blocks look like :

# coding: utf-8

import crappy

if __name__ == '__main__':

  gen = crappy.blocks.Generator([
    {'type': 'Constant', 'value': 1000, 'condition': 'delay=3'},
    {'type': 'Ramp', 'speed': 100, 'condition': 'delay=5', 'init_value': 0},
    {'type': 'Constant', 'value': 1800, 'condition': 'delay=3'},
    {'type': 'Constant', 'value': 500, 'condition': 'delay=3'},
    {'type': 'Sine', 'amplitude': 2000, 'offset': 1000, 'freq': .3,
     'condition': 'delay=15'}],
      spam=True,
      cmd_label='target_speed')

  mot = crappy.blocks.Machine([{'type': 'FakeDCMotor',
                                'cmd_label': 'voltage',
                                'mode': 'speed',
                                'speed_label': 'actual_speed',
                                'kv': 1000,
                                'inertia': 4,
                                'rv': .2,
                                'fv': 1e-5}])

  graph = crappy.blocks.Grapher(('t(s)', 'actual_speed'),
                                ('t(s)', 'target_speed'))

  pid = crappy.blocks.PID(kp=0.038,
                          ki=0.076,
                          kd=0.0019,
                          out_max=10,
                          out_min=-10,
                          i_limit=(-5, 5),
                          setpoint_label='target_speed',
                          labels=('t(s)', 'voltage'),
                          input_label='actual_speed')

As you can see, the Generator sends the target speed under the label 'target_speed', the Machine Block takes the 'voltage' label as a command and returns the 'actual_speed', and the PID Block takes both tha 'target_speed' and 'actual_speed' labels as inputs and returns the 'voltage' label. There is also a Grapher Block plotting both the 'target_speed' and 'actual_speed' labels. Also, notice the 'spam' argument of the Generator Block, that ensures that the Block sends the command at each loop, for a nice display on the graph. Let’s now link the Block together consistently :

# coding: utf-8

import crappy

if __name__ == '__main__':

  gen = crappy.blocks.Generator([
    {'type': 'Constant', 'value': 1000, 'condition': 'delay=3'},
    {'type': 'Ramp', 'speed': 100, 'condition': 'delay=5', 'init_value': 0},
    {'type': 'Constant', 'value': 1800, 'condition': 'delay=3'},
    {'type': 'Constant', 'value': 500, 'condition': 'delay=3'},
    {'type': 'Sine', 'amplitude': 2000, 'offset': 1000, 'freq': .3,
     'condition': 'delay=15'}],
      spam=True,
      cmd_label='target_speed')

  mot = crappy.blocks.Machine([{'type': 'FakeDCMotor',
                                'cmd_label': 'voltage',
                                'mode': 'speed',
                                'speed_label': 'actual_speed',
                                'kv': 1000,
                                'inertia': 4,
                                'rv': .2,
                                'fv': 1e-5}])

  graph = crappy.blocks.Grapher(('t(s)', 'actual_speed'),
                                ('t(s)', 'target_speed'))

  pid = crappy.blocks.PID(kp=0.038,
                          ki=0.076,
                          kd=0.0019,
                          out_max=10,
                          out_min=-10,
                          i_limit=(-5, 5),
                          setpoint_label='target_speed',
                          labels=('t(s)', 'voltage'),
                          input_label='actual_speed')

  crappy.link(gen, pid)
  crappy.link(mot, pid)

  crappy.link(pid, mot)

  crappy.link(gen, graph)
  crappy.link(mot, graph)

  crappy.start()

Note

To run this example, you’ll need to have the matplotlib Python module installed.

Can you see it ? We have both crappy.link(mot, pid) and crappy.link(pid, mot), which means that there is a feedback loop in the script ! The whole point of this section is to outline that feedback loops are not only possible in Crappy, but also necessary in some cases. Most of the time, it is in situations when a Block needs to modify its output based on the effect it has on another target Block. You can download this feedback loop example to run it locally on your machine. You can then tune the settings of the motor and see how the PID will react.

2. Using Modifiers

One of Crappy’s most powerful features is the possibility to use Modifiers to alter the data flowing through the Links. The rationale behind is that the data that a Block outputs might not always be exactly what you need. For example, data from a sensor might be too noisy and require some filtering. Or a command might have to be sent to two different motors, but with an offset on one of them. Such small alterations of the data should not necessitate to use a new Block, or to modify an existing one ! To deal with these minor adjustments, we created the Modifier objects.

The principle of Modifiers is that each Modifier is attached to a given Link. Every time a Block wants to send data through the Link, the Modifier alters it before it gets sent. A same Link can have several Modifiers attached, in which case they are called in the same order as they are given. Unlike the operations performed by the Blocks, the ones carried out by the Modifiers are not optimized at all. Therefore, Modifiers should only be used for simple tasks. The syntax for adding Modifiers is very simple, let’s get familiar with it in an example !

Starting from the example of the previous section, we now want to know the current position of the motor. To calculate this value, we just have to integrate the measured speed over time. This is numerically a very simple operation, since it is equivalent to a sum. It is thus a perfect job for a Modifier ! Luckily, Crappy already implements the Integrate Modifier for integrating a signal over time. Let’s add it on a Link starting from the Machine Block and pointing towards a new Grapher for the position :

# coding: utf-8

import crappy

if __name__ == '__main__':

  gen = crappy.blocks.Generator([
    {'type': 'Constant', 'value': 1000, 'condition': 'delay=3'},
    {'type': 'Ramp', 'speed': 100, 'condition': 'delay=5', 'init_value': 0},
    {'type': 'Constant', 'value': 1800, 'condition': 'delay=3'},
    {'type': 'Constant', 'value': 500, 'condition': 'delay=3'},
    {'type': 'Sine', 'amplitude': 2000, 'offset': 1000, 'freq': .3,
     'condition': 'delay=15'}],
      spam=True,
      cmd_label='target_speed')

  mot = crappy.blocks.Machine([{'type': 'FakeDCMotor',
                                'cmd_label': 'voltage',
                                'mode': 'speed',
                                'speed_label': 'actual_speed',
                                'kv': 1000,
                                'inertia': 4,
                                'rv': .2,
                                'fv': 1e-5}])

  graph = crappy.blocks.Grapher(('t(s)', 'actual_speed'),
                                ('t(s)', 'target_speed'))

  pid = crappy.blocks.PID(kp=0.038,
                          ki=0.076,
                          kd=0.0019,
                          out_max=10,
                          out_min=-10,
                          i_limit=(-5, 5),
                          setpoint_label='target_speed',
                          labels=('t(s)', 'voltage'),
                          input_label='actual_speed')

  graph_pos = crappy.blocks.Grapher(('t(s)', 'pos'))

  crappy.link(gen, pid)
  crappy.link(mot, pid)

  crappy.link(pid, mot)

  crappy.link(gen, graph)
  crappy.link(mot, graph)

  crappy.link(mot, graph_pos,
              modifier=crappy.modifier.Integrate(label='actual_speed',
                                                 out_label='pos'))

  crappy.start()

Note

To run this example, you’ll need to have the matplotlib Python module installed.

As you can see, the Modifiers are expected to be given to the 'modifier' argument of the crappy.link() function. Each Modifier has to be instantiated, and might require arguments. To know what the effect of a Modifier is, and which argument it takes, refer to the Modifiers section of the API. Here, the chosen Modifier is Integrate. It must be given the name of the label to integrate, and here the name of the label carrying the integral value is also specified. This new label is added to the data flowing through the Link, and can then be used by the downstream Block ! In the case of the Integrate Modifier, all the other labels are preserved.

As illustrated with this example, Modifiers are a simple yet powerful way to tune the data flowing through the Links. As the Modifiers distributed with Crappy will surely not cover all the possible use cases, we strongly encourage you to have a look at the section detailing how to code your own Modifiers. You can download this Modifier example to run it locally on your machine. The Modifiers distributed with Crappy are also showcased in the examples folder on GitHub.

3. Advanced Generator condition

In a previous section, the Generator Block and its Generator Paths were introduced. In that section, two possible syntax were given for the 'condition' key of a Path dict. The value None can be given, in which case the Path never ends. Alternatively, a str in the format 'delay=xx' can be given, in which case the Path ends after the specified delay. There is actually an other way to specify the stop condition, that we are going to detail in this section.

Getting right to the point, the third way to specify a stop condition is to give a str in the format 'label>value' or 'label<value'. Replace 'label' with the name of the label to monitor, and 'value' with a numerical value to compare the label with. The principle of this type of condition is that the Generator should be sent the label to monitor. At each loop, it checks if any point of the received label is below (or above) the given threshold. If that’s the case, the stop condition is met and the Path ends. The reason why this type of stop condition was not introduced in the section dedicated to Generators is that it requires the concept of feedback loop, that is only introduced earlier on this page !

Note

In the stop conditions given as str, you can freely add spaces around the =, < and > characters. The condition will still be recognized in the same way.

Note

And what about an '==' condition ? As in a vast majority of situations users are dealing with float, it is very unlikely that a label would be exactly equal to the given threshold ! Then what about something using math.isclose ? It could indeed come in use, but a similar behavior can be obtained using the < condition, a Modifier, and the abs function !

Let’s now use such a stop condition in an example. In the very first example of the tutorials, a delay condition was used for stopping the script. It was conveniently chosen so that the stop condition is met short after the sample breaks. But if the elongation pace is changed, the delay will for sure not match anymore with the sample failure ! Instead, we can use the new type of condition introduced here to always have the test stop short before the sample breaks, no matter the elongation speed. The code is as follows :

# coding: utf-8

import crappy

if __name__ == '__main__':

  gen = crappy.blocks.Generator(path=[{'type': 'Constant',
                                       'value': 5 / 60,
                                       'condition': 'F(N)>100000'}],
                                cmd_label='input_speed')

  machine = crappy.blocks.FakeMachine(cmd_label='input_speed')

  record = crappy.blocks.Recorder(file_name='data.csv',
                                  labels=['t(s)', 'F(N)', 'x(mm)'])

  graph_force = crappy.blocks.Grapher(('t(s)', 'F(N)'))

  graph_pos = crappy.blocks.Grapher(('t(s)', 'x(mm)'))

  crappy.link(gen, machine)

  crappy.link(machine, record)
  crappy.link(machine, graph_pos)
  crappy.link(machine, graph_force)
  crappy.link(machine, gen)

  crappy.start()

Note

To run this example, you’ll need to have the matplotlib Python module installed.

You can download this advanced Generator example to run it locally on your machine. Try to modify the value of the speed command, and see how the script always stops at the given condition. With the new type of stop condition for the Generator, you are now ready to use this block to its full extent !

Note

There is actually one more possibility to define custom stop conditions, that is much more advanced and is described in a later tutorial section.

4. Dealing with streams

In the tutorial section dedicated to IOBlocks, only the regular usage mode of the IOBlock was presented. In this mode, the data points are acquired from the In / Out object one by one, which results in a limited data rate usually around a few hundred samples per second. To circumvent this limitation, another acquisition mode was added for InOuts supporting it : the streamer mode. In streamer mode, the data rate of InOut objects can get as high as a few kHz ! This comes however at the cost of direct compatibility with most of the Blocks, as detailed below.

As you may have guessed, in streamer mode the data points are acquired and returned as chunks rather than individually. This means that the IOBlock sends multiple points at once to the downstream Blocks, which is totally unexpected for most Blocks. Therefore, only two objects in Crappy are natively compatible with the streamer mode : the HDF Recorder Block and the Demux Modifier. Let’s see with an example how to use these objects together !

The first requirement when using the streamer mode is to use an InOut supporting this mode. To know if that is the case, you need to consult the documentation for that InOut. Luckily, the FakeInOut InOut supports it, so we’ll use it here for the demo. And second, the streamer mode needs to be enabled on the IOBlock, via the 'streamer' argument. If these two conditions are met, the streamer mode is enabled ! The beginning of the example script looks as follows :

# coding: utf-8

import crappy

if __name__ == '__main__':

  io = crappy.blocks.IOBlock('FakeInOut',
                             labels=('t(s)', 'stream'),
                             streamer=True,
                             freq=30)

  rec = crappy.blocks.HDFRecorder(filename='data.hdf5',
                                  label='stream',
                                  atom='float64')

  graph = crappy.blocks.Grapher(('t(s)', 'memory'))

  stop = crappy.blocks.StopButton()

Notice how the 'streamer' is indeed set on the IOBlock. Except for that, the syntax for the IOBlock is the same as usual, and the HDFRecorder Block is also very close to the regular Recorder one. The differences are that instead of multiple labels to record, it only expects one stream label containing all the data at once. It also requires the expected data format to be specified. Now that the involved Blocks are instantiated, it is time to link them together :

# coding: utf-8

import crappy

if __name__ == '__main__':

  io = crappy.blocks.IOBlock('FakeInOut',
                             labels=('t(s)', 'stream'),
                             streamer=True,
                             freq=30)

  rec = crappy.blocks.HDFRecorder(filename='data.hdf5',
                                  label='stream',
                                  atom='float64')

  graph = crappy.blocks.Grapher(('t(s)', 'memory'))

  stop = crappy.blocks.StopButton()

  crappy.link(io, rec)
  crappy.link(io, graph,
              modifier=crappy.modifier.Demux(labels='memory',
                                             stream_label='stream',
                                             mean=True))

  crappy.start()

Note

To run this example, you’ll need to have the matplotlib and psutil Python modules installed.

Compared to the regular IOBlock usage, this is when things get a bit more complicated ! As the IOBlock and HDFRecorder are both meant to handle stream data, they can be linked together in a normal way. However, the Grapher Block cannot accept stream data, so the Demux Modifier must be added to their Link ! Basically, this Modifier accepts stream data as an input and outputs regular data usable by most other Blocks. Since streams might have a very high data rate, most of the information is discarded in the process to avoid overflowing the Link. Still, it outputs values that can be used for plotting or any other application. Here, the data from the IOBlock should be successfully displayed on the graph even though it originates from a stream.

You can download this streamer example to run it locally on your machine. The streamer mode is neither as used nor as well documented as the regular operation mode, so do not hesitate to request help on the GitHub page if you would have trouble using it !

5. Writing scripts efficiently

Because Crappy requires script with a specific syntax to run, users may forget that they can still make use of Python’s great flexibility and tools even inside scripts for Crappy ! This section is just a short and surely not exhaustive reminder of what is possible to do with Python in a script written for Crappy. Follow the given hints to write your scripts in a more efficient and elegant way !

5.a. Use variables

When providing arguments to a Block or any other object, remember that you can use variables instead of plain text or numbers. It will make your scripts easier for yourself and others to read and to modify.

Do not write :

record_pos = crappy.blocks.Recorder('tests/example/data/pos.csv')

record_force = crappy.blocks.Recorder('tests/example/data/force.csv')

record_extenso = crappy.blocks.Recorder('tests/example/data/ext.csv')

But write instead :

base = 'tests/example/data/'

record_pos = crappy.blocks.Recorder(base + 'pos.csv')

record_force = crappy.blocks.Recorder(base + 'force.csv')

record_extenso = crappy.blocks.Recorder(base + 'ext.csv')

5.b. Use loops

In a similar way as plain text or numbers can be replaced with variables, you can also replace list, dict, tuple and other collections with variables defined elsewhere. This is particularly interesting if you have big objects, that can be generated following a known patter. In that case, using loops will save many lines and avoid typos. If you’re familiar with the concept of comprehension, it can also help you make your code even more compact than with loops !

Don not write :

gen = crappy.blocks.Generator([
    {'type': 'Constant', 'value': 0, 'condition': 'delay=5'},
    {'type': 'Constant', 'value': 1, 'condition': 'delay=5'},
    {'type': 'Constant', 'value': 2, 'condition': 'delay=5'},
    {'type': 'Constant', 'value': 3, 'condition': 'delay=5'},
    {'type': 'Constant', 'value': 4, 'condition': 'delay=5'},
    {'type': 'Constant', 'value': 5, 'condition': 'delay=5'}])

But write instead :

path = list()
for i in range(6):
    path.append({'type': 'Constant', 'value': i, 'condition': 'delay=5'})

gen = crappy.blocks.Generator(path)

Or even more concise :

gen = crappy.blocks.Generator([
    {'type': 'Constant', 'value': i, 'condition': 'delay=5'}
    for i in range(6)])

5.c. Use other packages

Even though in all the examples and tutorials Crappy is the only package to be imported, it is totally fine to import and use other packages in your script ! This can be convenient for performing operations before Crappy starts, or after it ends. One of the best example of a module that can come in use in a script is pathlib. It handles file paths in a cross-platform compatible way, so that you don’t have to care about / and \ if you want to make your code runnable on both Linux and Windows. It is also part of the standard library of Python, so it doesn’t need to be installed.

Do not write :

record_pos = crappy.blocks.Recorder('tests/example/data/pos.csv')

record_force = crappy.blocks.Recorder('tests/example/data/force.csv')

record_extenso = crappy.blocks.Recorder('tests/example/data/ext.csv')

Using pathlib, write instead :

from pathlib import Path

base = Path('tests/example/data')

record_pos = crappy.blocks.Recorder(base / 'pos.csv')

record_force = crappy.blocks.Recorder(base / 'force.csv')

record_extenso = crappy.blocks.Recorder(base / 'ext.csv')

6. Using Crappy objects outside of a Crappy test

In the new section of the tutorial, let’s see how you can use the classes distributed with Crappy to interact freely with hardware outside the context of a Crappy test (i.e. without calling crappy.start() or an equivalent method). But first, why would you do that ? Well, while Crappy is a nice framework for running entire experimental protocols, it is a bit cumbersome if you only want to acquire one image or one data point from a sensor. That’s why this “hack” is presented here ! Note that it is truly not an intended feature of Crappy, but rather a consequence of its implementation.

In Crappy, the In / Out, Actuators and Cameras objects each implement the code needed to interact with a specific equipment. They make sure that this code is organized and can be called in a standard way, so that it can be used by the IOBlock, Machine and Camera Blocks respectively. Knowing how to properly call the corresponding code, it is thus possible to use these classes to directly interface with hardware outside the context of a Crappy test.

To learn more about the mandatory and optional methods that each class can implement, you should refer to the Creating and using custom objects in Crappy page of the tutorials. Here, a very basic example will be used to demonstrate how a Camera object can be used for acquiring and visualizing images. The FakeCamera will be used, so that no hardware is required to run the script. The trick to use this class directly is to instantiate it, without using a Camera Block. Let’s write the first part of the script :

# coding: utf-8

import crappy

if __name__ == '__main__':

  cam = crappy.camera.FakeCamera()
  cam.open(width=1280, height=720, speed=100, fps=50)
  img = cam.get_image()[1]

As you can see, the FakeCamera is directly instantiated whereas normally its name would have been given as an argument to a Camera Block. Then, the open() and get_image() methods are called for respectively initializing the Camera and acquiring an image. The detail of the methods exposed by the FakeCamera and their exact syntax have to be looked up in the API. Notice how the arguments to provide to the FakeCamera are passed to the open method, instead of being given to Camera Block. As you may have guessed, the script above initializes a FakeCamera, and acquires one image from it. As This is not very interesting to watch, let’s add some visualization :

# coding: utf-8

import crappy
from cv2 import imshow, waitKey

if __name__ == '__main__':

  cam = crappy.camera.FakeCamera()
  cam.open(width=1280, height=720, speed=100, fps=50)
  img = cam.get_image()[1]

  imshow('picture', img)
  waitKey(3000)

Note

To run this example, you’ll need to have the matplotlib and opencv-python Python modules installed.

You can download this FakeCamera example to run it locally on your machine. With the visualization added, it should now acquire a picture from the FakeCamera, display it for 3 seconds and return. With this example, we managed to use a Camera object without ever calling crappy.start(). Note that the same principle applies to InOut and Actuator objects, the Camera was only used here because it is more visual.

7. Advanced control over the runtime

For the last section of this tutorial page, let’s see how you can achieve a finer-grained control over Crappy’s runtime. There are two ways to control Crappy in a more accurate way : passing arguments to crappy.start(), and/or using alternative startup methods.

7.a. Alternative startup methods

So far, the only option that was presented for starting a script in Crappy was to use the crappy.start() method. There are actually more options available, that can be used in very specific situations.

If you look inside the start_all() method, that is the alias behind crappy.start(), you’ll see that it is just made of three consecutive calls to prepare_all(), renice_all() and launch_all(). These methods are aliased to crappy.prepare(), crappy.renice() and crappy.launch() for being called by the user in a script. To get an exact description of what each of these methods do, refer to the Developers information section of the documentation. In short, the crappy.prepare() method initializes all the Blocks, but does not start the test. For example, after calling this method, the actuators are powered on, the sensors are configured, and the files for recording data are created. The crappy.renice() method can be ignored by most users. And the crappy.launch() actually starts the test and is blocking, just like crappy.start().

But why would you want to split up the three methods of crappy.start() ? By doing so, you gain the possibility to add some code between the crappy.prepare() and crappy.launch() methods. This mostly gives you the capacity to interact with hardware once it is initialized but the test is not yet started. For example, it is used on some setups to allow the user to place samples on the device once the motors reach an initial position. On other setups, we use it to drive an actuator in manual mode, and only start the test once the desired position is reached and the actuator is switched back to software-controlled mode.

Warning

If code is included between the crappy.prepare() and crappy.launch() methods, there is no warranty that Crappy terminates gracefully in case this code crashes ! Be extremely cautious when performing operations that can potentially fail, and make sure to understand what the effects would be on your setup !

As the alternatives crappy.start() are much more difficult to use in a safe way, and have very few clean use cases, no example will be showed for this section. We consider that users skilled enough to use these methods safely should be able to do so without an example. Still, these methods exist and are part of the API, and as such they are presented in this tutorial section.

7.b. Arguments to the startup method

The crappy.start() method, alias to the start_all() method of the class Block, accepts three arguments that can help customize a bit the behavior of Crappy. They are briefly detailed in this section.

The first possible argument is 'allow_root', which is a bool. If set to True, it allows renicing the Blocks to negative nicenesses. To do so, the root access will be requested. It only applies on Linux, and if a negative niceness was attributed to a custom-written Block. It is therefore a very specific setting that most users can ignore and leave to False.

The second argument is 'log_level', that can accept the values logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL, or None. The given value corresponds to the maximum level of the log messages displayed in the console and recorded in the log file. Logging can also be totally disabled, by setting it to None. This argument does not have many actual use cases, except maybe for making Crappy silent, or to better spot the errors by disabling the messages with inferior priority. In the general case, it is advised to leave this argument to its default value.

Finally, the 'no_raise' argument is a bool that allows to disable the exceptions raised at the end of a script. The default behavior of Crappy is to raise an exception when it stops, if either an unexpected error was raised during its execution or if a KeyboardInterrupt was caught (script stopped using Control-c). The purpose of this behavior is to prevent the execution of any line of code that would come after crappy.start(), since it might not be safe to run it after Crappy has failed or the user interrupted the test. By setting 'no_raise' to True, the exceptions are disabled and Python goes on after Crappy finishes, even if it crashed. Use this feature with caution, as it can lead to unexpected or even unsafe behavior ! This argument can be changed by users who would prefer to use Control-c to stop tests but don’t want exceptions to be raised, although we discourage using this strategy.