Blocks and Arrays Tutorial¶
This tutorial will walk through modeling and simulating a simple system using CertSAFE’s blocks features. Blocks are CertSAFE’s mechanism for representing arrays, loops, and iterated logic. See the Blocks and Arrays article for a more complete description of the available features. You can download a completed version of the example project from this tutorial to help you follow along.
Suppose you are designing a temperature display system that has the following requirements:
Temperature Display Requirements:
- The Temperature Display shall have 10 digital temperature sensors. Each temperature sensor shall provide its current temperature in Celsius as a 32-bit floating point value.
- The Temperature Display shall display the current temperature from each sensor in degrees Fahrenheit. The temperature °F shall be calculated from the temperature °C with the following equation: °F = °C × 9/5 + 32
One option for modeling this system in CertSAFE would be to write the logic for processing one sensor and then copy and paste that logic nine additional times. This is less than ideal, because changing the behavior of all of the sensors requires manually updating each one.
A better choice would be to create a custom component that encapsulates the logic for a single sensor, then create 10 instances of that custom component. This makes it possible to change the logic in only one location to affect all of the sensors simultaneously. However, this still requires manually connecting 10 copies of the custom component to their appropriate inputs and outputs.
The blocks notation solves these problems by allowing a single wire in a diagram to carry an array of multiple values. Using blocks is appropriate when you have a large number of items whose processing should behave identically or with only minor differences. We will show how the requirements above can be modeled in CertSAFE using blocks notation.
The simplest sort of array operation is applying a function to each element of an array independently (pointwise). In CertSAFE, this happens automatically when you connect an array-typed variable to pure logic such as arithmetic or Boolean operations. CertSAFE will automatically apply such pure logic pointwise to each element of the input array to produce an output array of the same size.
In our example, we just need to write the logic for converting a single value in Celsius to the corresponding value in Fahrenheit. If we then pass in an array of 10 Celsius values, we will get out an array of the 10 corresponding Fahrenheit values.
How do we specify that we want to pass in an array of 10 values? By default, if a model contains only pure components, CertSAFE assumes that we do not want any arrays when we go to simulate the model. We can force CertSAFE to add an array dimension to the variables in our model using a block type annotation. This works similarly to adding a type annotation for the ordinary scalar type of a variable, except that we are specifying the types of block dimensions instead.
To add a block type annotation, select a Network Annotation component, such as the degrees Celsius input in our diagram,
and edit its Index Types property in the Properties view. In our case, we want to add a single block dimension whose
[1..10], an integer interval type with 10 elements. We
can also add a scalar
Float32 type annotation on the same Network Annotation component. When both annotations are
shown together in the user interface, they appear as “[1..10] ⊢ Float32”.
When you add a block type annotation to a network, you are specifying the types of the network’s innermost block dimensions. However, the network may still have additional outer block dimensions. This distinction is only relevant when working with multidimensional arrays. See the Blocks and Arrays article for more details.
With the type annotations, the final diagram for requirements 1 and 2 looks like this:
Viewing arrays in simulations¶
Now that we have a model describing our temperature display system, we would like to be able to simulate it. Creating a simulation for this model works exactly the same as for any other model: set the definition as root, create a new simulation, and add waveforms to the timeline by dragging variables from the Instance view.
The difference is in how to interact with the waveforms once you have added them. CertSAFE’s waveform plots can only show a single dimension of data, namely time. In general, a CertSAFE array can have multiple block dimensions as well as containing values that change over time, so a one-dimensional plot by itself is clearly inadequate to interact with array data. In order to view all of the data in an array-typed variable, we have to slice to a particular slot in the array, after which we can view the time history of the values at that single array position.
This concept of “slicing” is analogous to using the time cursor in the simulation timeline to pick out a point in time and look at its values. When looking at a probe in a diagram, we slice away all of the dimensions of the variable including the time dimension, leaving only a single value. When looking at the waveform for a variable, we slice away all of the dimensions of the variable except for the time dimension, leaving a one-dimensional plot.
Whereas slicing on the time dimension is performed using the time cursor, slicing on block dimensions is performed using the Block Indices view. This view shows a list of slider controls, which are automatically generated to correspond with the array types in your model.
As with other views, if the Block Indices view is not currently visible in your CertSAFE window, you can show it using the View menu.
In the simple temperature display system that we have at the moment, the Block Indices view will only show a single
slider. This slider corresponds to the
[1..10]-typed array dimension, with the possible positions of the slider
control being the values 1, 2, ..., 10. You can edit the position of the slider either by dragging the handle left and
right, or by clicking on the text box in the upper-right corner and typing in a new value.
The Block Indices view automatically filters the displayed sliders based on the current selection in the currently-focused editor. This is similar to how the Properties view changes contents dependending on the current selection in the currently-focused editor. If you have the Block Indices view open but it is empty, make sure that you have a simulation editor focused and that you have added at least one array-typed variable to the simulation.
CertSAFE uses the structure of your model to try to guess which variables should share sliders with each other. Sharing of sliders is useful because it lets you see values at corresponding positions in related variables. For example, if you are looking at the input Celsius data from temperature sensor #5, CertSAFE will guess that you probably also want to view the output Fahrenheit data from temperature sensor #5, rather than the output data from (say) temperature sensor #3. This spares you from having to manually synchronize which slot you are looking at between the two variables, and is especially useful when working with large datasets.
CertSAFE automatically assigns ID numbers to the sliders it generates, so that you can cross-reference the array
dimensions in your model with the corresponding sliders. When an array-typed variable is shown in a simulation, the
slider IDs for that variable are displayed in colored boxes next to the variable name. One slider ID is shown for each
of the variable’s array dimensions, in the same order that the dimensions occur at the type level. In our simple case,
the single slider is automatically assigned ID
1, and so boxes with the number
1 are displayed in the baselines
of the waveforms for the “current temp (°C)” and “current temp (°F)” variables when they are added to a simulation.
When you place a diagram editor into instance mode, each probe will also show the slider IDs for its associated network.
Given a time cursor position and block slider positions, each probe will show only the single value at the designated time and array slot. Similarly, given block slider positions, each waveform will only show the values over time at the designated array slot. To see the values at another array slot, you have to move the sliders to the desired position. This also applies for setting IntelliPoints: each slot of an array-typed controlled variable has its own IntelliPoints.
For more information on using array-typed variables in simulations, see the article on working with blocks in simulations.
Folding over arrays¶
Let us add an additional requirement to our temperature display system.
Temperature Display Requirements:
- The Temperature Display shall display the difference between the maximum and the minimum of the 10 current temperatures reported by the temperature sensors, in degrees Fahrenheit.
This behavior cannot be written using only pointwise operations. We need a way to accumulate information from all of the values in the array to produce a single output.
In CertSAFE, this can be done using block fold primitives. A block fold takes an array as input and applies a binary operation repeatedly to combine all the elements of an array into a single output value. You can add a block fold primitive to your diagram as follows:
- Drag out a varargs primitive from the Palette, such as a Min/Max Value Selector or Boolean AND/OR/XOR.
- With the varargs primitive selected, find the Inputs property in the Properties view.
- Click to edit the Inputs property. Instead of specifying a number of inputs, click on the block button in the editor, then click outside of the Properties view to apply the edit. This changes the primitive to a block fold on block level 1.
The result is a primitive with a single input, labeled with a λ (lambda) symbol. This indicates that the component consumes an array as input rather than individual scalar values.
In our case, we want a Max Value Selector block fold and a Min Value Selector block fold. You can make a Max Value Selector fold and then copy the component and change the copied component to a Min Value Selector through the Properties view. The final diagram for this requirement looks like this:
Using scalar values with array operations¶
Here are some more requirements for our temperature display system.
Temperature Display Requirements:
- The Temperature Display shall receive a threshold temperature in Celsius as a 32-bit floating point value.
- The Temperature Display shall show an error for each temperature sensor whose current temperature exceeds the threshold temperature.
Notice that the threshold temperature is a single scalar value, not an array. CertSAFE’s type system only allows pure components such as the Comparator to be used if all of the inputs and outputs are arrays of the same size. Therefore, in order to compare each of the elements of an array against a single scalar value, we need to replicate the scalar value out to an array of the same size.
The Capture primitive does exactly this: it adds a new array dimension by replicating the input value repeatedly. The
size of the new dimension on the output of the Capture component is automatically inferred by the type system in order
to match whatever the output wire is connected to. If the output array is inferred to be of size 10, then giving an
input value of
5.0 to the Capture component results in the output array
[5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0].
We can use the Capture primitive as shown below to implement requirements 4 and 5.
Why did we not have to use a Capture component with the literal values
5 in the Celsius-to-Fahrenheit
conversion diagram before? The reason is that CertSAFE’s Literal primitive is block polymorphic. This means that
connecting a Literal component with a value of
9 directly to logic that requires an array of size 10 will
automatically produce the array
[9, 9, 9, 9, 9, 9, 9, 9, 9, 9] without needing an explicit Capture component.
If you are working with blocks notation and CertSAFE gives a type error message along the lines of...
The block prefixes Γ and (Γ, [1..10]) are incompatible with each other. Perhaps you are mixing variables with different numbers of dimensions incorrectly.
...one possible cause is that you need to add a Capture primitive so that CertSAFE knows which values are scalars, which values are 1-D arrays, which values are 2-D arrays, etc. Remember that you can hover the mouse over a wire to see the type that CertSAFE has inferred for that variable. See the Blocks and Arrays article for details about how to read block types in the CertSAFE user interface.
Pack Block and accessing the current index¶
Here is yet another requirement for our temperature display system.
Temperature Display Requirements:
The Temperature Display shall show an alert for each temperature sensor whose current temperature in degrees Celsius exceeds
ALERT_VALis a constant that is different for each temperature sensor, defined by the following table:
Sensor No. 1 2 3 4 5 6 7 8 9 10
150 170 120 150 165 90 200 150 150 250
The interesting part here is that the behavior varies between the different sensors. There are at least two ways we can implement this logic in CertSAFE.
One option is to use the Pack Block primitive. This component takes a number of scalar values and assembles them into a 1-D array. (It can also take multiple 1-D arrays and assemble them into a 2-D array, etc.) We can use a Pack Block component to assemble Literal values into a length-10 array, then compare that array pointwise against our array of current temperature values.
Note the default pin on the Pack Block component. Since several of the values in the table were 150, we can collapse them down to a single case which is used if none of the other cases apply. The default pin is optional; we could have just written all 10 values explicitly.
The Pack Block component is convenient when you want to fill an array with a small number of items. It also allows the
input values to be dynamically computed, although we are only using constant values in this example. However, Pack Block
scales poorly to writing larger constant arrays because we have to manually add lots of individual Literal components in
our diagram. An alternative option is to use the Index primitive. This component outputs an array whose value at each
position is the current array index. For example, if the key type of the array is the integer interval
the Index primitive will output the array
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]. You can use this to perform different
logic depending on the current loop index. For our situation, we can make a
case table containing the data from requirement 6, then use the Index component to
supply the appropriate input index to the table for each array position. Case tables are pure, so they are
automatically replicated over the input array just like Boolean or arithmetic operations. See the
table tutorial for more information about creating case tables.
Indexing into arrays¶
Let us add one more set of requirements to our temperature display system.
Temperature Display Requirements:
- The Temperature Display shall receive 5 reference temperatures in Celsius as 32-bit floating point values.
- Two temperature sensors shall be associated with each reference temperature as follows: sensors 1 and 2 with reference temperature 1, sensors 3 and 4 with reference temperature 2, and so on. The Temperature Display shall show a warning for each temperature sensor whose current temperature exceeds its associated reference temperature.
In this situation our inputs are an array indexed on
[1..10] (the current temperatures) and an array indexed on
[1..5] (the reference temperatures), and we need our output to be an array indexed on
[1..10] (Boolean flags
indicating which sensors have warnings). We want to use the index of the output value we are currently processing to
compute the corresponding index of the reference temperature to access, and then look up that reference temperature from
the input array.
Given the current output index, we can compute the corresponding reference temperature index with some simple
arithmetic. CertSAFE does not allow arithmetic operations to be performed directly on integer interval types, but we can
apply the Nearest primitive to convert from
[1..10] to a binary integer data type such as
Int32, then perform
the arithmetic operations, and finally use another Nearest primitive to convert back to the desired result type
Since there are a relatively small number of indices in this example, an alternative option would be to just write a case table or Switch primitive listing all 10 sensor indices and the corresponding reference index that goes with each one. This style of modeling is especially useful when working with enum-indexed arrays, as opposed to the interval-indexed arrays we are using in these examples.
To actually look up the reference temperature from the input array, we need to reach for another new primitive called the Block Switch. The Block Switch takes an array and an index and outputs the value of the array at that index. We can use the Index primitive to get the current output index, use the arithmetic logic from above to compute the appropriate index to look up in the reference temperature array, and then use the Block Switch to actually perform the lookup.
A subtle detail is that we cannot just pass the
[1..5]-indexed reference temperatures array as the left-hand input
to the Block Switch component. This would remove the
[1..5] dimension, but it would not give us the needed
[1..10] dimension on the output. The solution is to add a Capture component on the left-hand input of the Block
Switch that inserts the required dimension before the
[1..5] dimension. This gives us a two-dimensional
[1..5] array, then cuts out the inner dimension with the Block Switch, leaving only a one-dimensional
array indexed on
[1..10]. Another way of thinking about it is that we are bringing the
[1..5]-indexed array into
the correct scope (capturing it) so that we can access it inside the
If you are not sure which reindexing component is appropriate for a given situation, try hovering the mouse over components and wires to view their types. You can typically figure out which component is appropriate by comparing the type signatures of the various primitives with the types that you want the variables in your diagram to have. This is an advantage of static typing compared to other matrix-based languages that are dynamically typed, and it is especially helpful when working with multidimensional data.
We need to use a Capture component that inserts the new dimension at block level 2—rather than the default of block level 1—so that the new dimension is inserted before the existing dimension. You can obtain such a component by dragging out a default Capture component from the Palette view and then using the Properties view to edit the component’s Block Level property from 1 to 2.
This combination of Capture and Block Switch is very common, so CertSAFE also includes a specialized primitive called Compose just for this purpose. The Compose component effectively uses the array(s) on the top of the component to reindex the array on the left of the component, composing the arrays together (like function composition). A Compose component is equivalent to some number of Captures together with some number of Block Switches, with the exact configuration dependening on the settings of the Compose component’s properties. Compared to using a combination of Captures and Block Switches, the Compose primitive takes up less space in your diagram and omits the extraneous intermediate variables.