v5.0 / 4 of 23 / 01 sep 99 / gvg
* Those who just start out programming with VEE and aren't familiar with its unique programming capabilities will often resort to programming approaches they have refined with other languages: they'll write programs with loops that perform computations one at a time.
The result is a very slow implementation. VEE, however, is optimized towards operations on arrays, and a knowledge of how to use array math to attack such problems can result in programs several orders of magnitude faster. This chapter outlines VEE array math techniques.
For example, consider the simple program that follows (see xarrcos.vee
for source for this example and the one after it):
Now let's do the same thing a different way:
There is a catch to this implementation: the operations are not done on a
one-by-one basis, each step in the operation works on an entire array at a
time. While it replaces operations that were being done in a loop, it's not
performing any loop itself, and if you have an operation that requires the
results of one iteration of the loop in a later iteration of the loop, using
this array approach may be difficult or impossible.
* VEE has considerable capability to perform math operations on entire
arrays. For another example, suppose you want to get the area under a curve
from a set of input data. There is a simple algorithm for doing this named
"trapezoidal rule", in which you just take the average height between each
pair of data points, multiply each average by the interval between the
points, then sum all the results.
VEE's array operations make it easy to implement trapezoidal rule in a single
function (if you assume a constant interval between data points). To
illustrate (see xgenfunc.vee for the source):
* This ability to manipulate arrays makes the VEE formula object very
powerful, though the techniques for doing so are nothing most people would
figure without a few clues to help. This chapter articulates some of the
things that can be done as a guide; it is hardly comprehensive, however,
since the number of tricks that can be played to provide a faster solution
are effectively unlimited.
I have heard that the old APL language had a similar capability to manipulate
arrays and do powerful things with one or two lines of code. This
information, unfortunately, came along with the caution that APL is
notoriously obscure. Some called it a "write-only" language. If you want
to perform sophisticated formula manipulations in VEE, I would suggest you
exercise care and document your path thoroughly lest you end up with
something nobody else (or even yourself, a year down the road) can figure
out.
* The first element in advanced VEE programming is learning how to handle
arrays. VEE has a highly flexible scheme for accessing arrays and parts of
arrays. For example, consider a one-dimensional array accessed with the name
"A"; you can select element 1 with:
Most elementary VEE math functions, such as "log()", "sin()", "cos()", and so
on, can accept an array as a parameter and return an array. Of more
specialized functions, some will not perform array math and some (a few of
which we will discuss in the following text) are particularly handy for it.
For examples of functions that are useful in array operations:
For operating examples of VEE array math, please check the examples
xarrmth1.vee and xarrmth2.vee.
* As an example of what can be done with VEE array manipulations, an HP field
person had a VEE user who wanted to read a 2-dimensional array from a file,
take one column of the array, perform some math on the data, and put back the
column in the array.
All that had to be done in this case was use a single Formula box with the
formula:
* For a second example, a user sent in a request to us that involved
displaying the elements of a 2048-element array as 16 sets of 128 elements
each. While the question focused on the display of the data, rather than its
generation, I got to thinking about the task of breaking up the array.
It was really quite simple. The following program (see xarrchop.vee for
the source) shows how it's done:
* A third example involved a user wishing to concatenate multidimensional
arrays. VEE has a Concatenator object, but it can only generate a
one-dimensional output. However, a Formula box can be used to do this
function as long as the number of rows (or columns) is fixed (a
constraint that is usually met in practice). The following Formula box
performs this operation on a pair of arrays that have two rows:
* For a fourth, more sophisticated example, consider the problem that one of
our European field representatives handed to us. His user wanted to multiply
a vector:
The operation here is effectively a scalar multiplication of each row of the
matrix by each element of the vector, and with that known the implementation
is very simple, using the same techniques outlined so far.
To demonstrate, consider a data set consisting of a vector of the form:
* For a fifth example, a particularly useful function to perform is to insert
one or more data elements in an existing array. It is simple to build a
UserFunction to do this job (see the xgenfunc.vee library for the
source):
* A related (if simpler) example is building a data queue with array
operations. A queue is essentially an array of fixed length, where new
elements are added at one end, with the numbers shifting down to the other
end, where numbers fall off and are lost.
This can be done with surprising directness in VEE. Consider the following
program (see xarrcue.vee for source):
But suppose you can add data arrays of any length and the first array doesn't
give the size of the queue? Then you can update the UserFunction as follows:
* For yet another example, a VEE user wanted to find a particular value in an
array. The obvious answer was to use a Comparator, but another clever user
also point out that if he simply wanted to see if the value existed, he could
use an If/Then/Else object as follows:
Of course, this same trick can be used in a formula box:
Suppose you want to convert a vector (one-dimensional array) into a matrix
(two-dimensional array). All you have to do is run the vector through a From
String and specify the array format you like as an output. For example (see
xarrange.vee for the source):
* One of the trickier scenarios in VEE array operations that it can
nonetheless often deal with are operations where comparisons must be made on
entire arrays of data.
For example, consider a scenario where a VEE user wants to take several data
sets from a device and get a resulting data set that consists of the maximum
values from all the individual data sets.
The following demo program -- the program source is in the file
xarrgtmx.vee -- shows off the method required:
Hitting the Get Data button gets a new waveform from a Noise Generator, then
recovers the data in "sumdata" with a Get Global object. These two waveforms
are summed into a Formula box, which processes them and shoves the result
back into "sumdata" with a Set Global.
The Formula box is the key. It accepts the new data on pin A and "sumdata"
on pin B, and all it has in it is:
* As another (somewhat trickier) example, a user wanted (as a benchmark) to
compare two arrays of random numbers and determine how many numbers in the
first array were greater than those in the second array.
Comparing arrays would seem difficult, since the relational operators ("==",
"!=", "<=", and so on) and the triadic operator ("A < B ? C : D") will only
return scalar values (the original lab team decided that the meaning of these
functions for arrays was ambiguous), but there are ways to fake it. The test
was performed with the following program -- see xarrrncp.vee for the
source:
This provides an array where values are positive if A > B, zero if A == B,
and negative if A < B.
The "signof()" function converts positive numbers into 1, negative numbers
into -1, and leaves 0 at 0. (An astute reader will be able to see, as
shall be related, this provides enough information to perform all the
conventional relational operations.)
This strips all the -1 values out of the array. Now you have an array
which is 1 if A > B and 0 otherwise.
Of course, "sum()" then can add up the 1s and give the number of comparisons
where A > B.
* By extension, all the conventional relational operators can be implemented
using techniques demonstrated above:
For examples, check the program file xarrrelt.vee.
* However, you don't necessarily want to try to construct huge logic
operations inside a formula box. The idea is mainly to eliminate or reduce
loops, not come up with a minimum number of objects, and using multiple
objects or implementing some of these functions in User Functions is
preferable to building something that cannot be maintained. Furthermore, a
good understanding of array-manipulation techniques can allow you to bypass
formal logic operations and come up with a more direct solution.
For example, we had a user who had received an array of 8-bit unsigned data
back from an I/O device. It had been converted by VEE into 32-bit signed
integer data and he wanted to get the real values back.
This could be done by adding 256 to each value of the return array; a single
formula did the task -- check the program file xarrcvt.vee for an
operational example:
Convert all positive values to 0.
Convert all negative values to -1 (recall that the inputs are integers,
not reals). This now results in an array that has -1 for each negative
value and 0 otherwise.
Multiplying that array by -256 results in an array that has 256 for each
negative value and 0 otherwise. This array can then be added to the
original array to offset all the negative values to their "true" positive
value.
This example could have been done using relational operations as constructed
previously, but it would have been much more complicated. A little
understanding of what needs to be done can lead to the more direct solution.
* Using these cute Formula box operations can lead to a myopic obsession with
one's own cleverness and hideous and unnecessary complications. The Formula
box can do quite a bit but it should not be regarded as the ultimate answer,
and other tools should be used or added as the need arises.
A particularly useful tool for array computations is the Comparator object,
which allows extraction of such array elements as meet a specific criteria.
For example, one user wanted to determine the "transitions" in a stream of
data. Given an example array of data as follows:
The solution is as follows -- see xarrtnst.vee for the source:
Note that the data obtained in the subtraction not only indicates the index
of the transaction, but its direction -- 1 for a positive transition, -1 for
a negative. Math freaks will recognize this operation as basically a
difference-equation approach to performing a differentiation.
[4.1] ARRAY PROCESSING OVERVIEW
[4.2] VEE ARRAY OPERATIONS
[4.3] ADVANCED ARRAY OPERATIONS
[4.1] ARRAY PROCESSING OVERVIEW
+-----------+
| For Count | +-----+
| 360 +--+->| sin +----->+-----------+
+-----+-----+ | +-----+ | Collector +---->+-----+
| | +-->+-----------+ | JCT +---->+-----------+
| | +-----+ | +->+-----+ | Collector |
| +->| cos +--|-->+-----------+ | +->+-----------+
| +-----+ | | Collector +--+ |
| +-->+-----------+ |
| | |
+--------------------+------------------------------+
This generates an array containing all the values of sine and cosine for each
degree from 0 to 360. On my PC this takes about 2 seconds to execute.
+-------------------------------------------------------+
| Formula |
+-------------------------------------------------------+
| [ sin(ramp( 360, 0, 359)) cos(ramp( 360, 0, 359 )) ] |
+-------------------------------------------------------+
This is about 40 times faster. However, it's not intuitively obvious what is
happening here, so some explanation is required:
+------------+
| Real |
+------------+
| 0 |
| 1 |
| 4 |
| 8 +---+
| 5 | |
| 3 | |
| 6 | | +----------------+
| 2 | | | Formula |
+------------+ +-->+----------------+ +-------+
| +-->| 28 |
+------------+ +-->+----------------+ +-------+
| Real | |
+------------+ |
| 1 +---+
+------------+
The trapezoidal rule formula accepts a data array in pin A and the data
interval in pin B:
sum( B * ( A[0:totsize(A)-2] + A[1:totsize(A)-1] ) / 2 )
The "totsize()" function gives the length of the array, so this formula adds
each one of the data values to its successive value, divides by 2 to get the
average value, multiplies by the width to get the area of each slice, and
then uses the "sum()" function to add up all the averages and get the total
sum.
[4.2] VEE ARRAY OPERATIONS
A[1]
-- and elements 0 through 5 with:
A[0:5]
You can select all the array elements from 5 to the end of the array with:
A[5:*]
If the array has multiple dimensions, you can use the "*" to select entire
rows or columns:
A[1,*]
* Math on arrays follows another set of simple rules. Elementary scalar
arithmetic operations on arrays simply perform their operations on each
element in the array:
A*2
-- multiplies each element in the array by 2; and:
A-4
-- subtracts 4 from each element in the array. Such arithmetic operations
between two arrays that have the same size and dimensions perform the
operation between corresponding elements of the arrays. For example, if A
and B are two arrays, then:
A*B
-- multiplies each element of the array by the corresponding element of the
other. This does not perform a "matrix multiply", which is a relatively
complicated multiplication of rows times columns and summation that results
in a scalar (VEE has a function to do that).
ramp() As shown earlier, can be used to generate "loop" counts.
concat() Concatenates two arrays & returns a 1-dimensional array.
totSize() Gives total number of elements in an array.
signof() Sign of value ( -1 / 0 / 1 if <0 / 0 / >0).
abs() Absolute value.
rotate() Rotates elements in array.
sum() Sums all elements in an array.
sort() Sorts an array.
randomize() Generates array of random numbers.
min() Minimum value of data set.
max() Maximum value of data set.
clipUpper() Clips below maximum given value.
clipLower() Clips above minimum given value.
* A very useful feature in Formula boxes is the ability to define expressions
as arrays, as illustrated in the example in the first section:
[ 2*B 1/B B*B log(B) ]
This expression generates an array containing the double, reciprocal, square,
and natural log of the input named "B". Note that if you have an expression
with a minus sign in it, you have to enclose it in parentheses:
[ 2*B 1/B (-B*B) log(B) ]
-- since otherwise VEE will interpret this as "1/B - B*B".
+-----------------------------+
| Formula |
+---+---------------------+---+
-->| A | [ A[0,*] 4*A[1,*] ] | R +-->
+---+---------------------+---+
This Formula box essentially dismantles the input array, multiplies the
second column by 4, then glues it back together.
+----------------------+
| ramp( 2048, 0, 2047) +--+ +----------------------------+
+-----------+----------+ | | Formula |
| | +---+------------------------+
+--------+-------+ +-->| A | A[B*128:(128*(B+1)-1)] +-->
| For Count: 16 +-------->| B | |
+----------------+ +---+------------------------+
The "ramp()" function simply generates a 2048-element array with values from
0 to 2047 for test purposes. The For Count object ticks off each of the 16
individual arrays to be generated, while the Formula box selects the
appropriate sub-array using indexes generated from the count:
A[0:127], A[128:255], A[256:384], ... , A[1920:2047]
Of course this assumes a fixed array size, number of subarrays, and size of
subarrays. Get any of them wrong and you get an error box.
+---------------------------------------------------------+
| Formula |
+---+-------------------------------------------------+---+
-->| A | [ concat(A[0,*],C[0,*]) concat(A[1,*],B[1,*]) ] | R +-->
| B | | |
+---+-------------------------------------------------+---+
The key to this is the "concat()" function, which concatenates two arrays and
produces a one-dimensional array. This Formula box strips out the rows of
each of the arrays, concatenates them, and then joins them back together. To
access a program demonstrating this function, check the xarrcat.vee file.
[ X1 X2 X3 X4 ]
-- times a matrix:
[ y11 y12 y13 y14 ]
[ y21 y22 y23 y24 ]
...
[ y21 y22 y23 y24 ]
-- to get the result:
[ X1*y11 X2*y12 X3*y13 X4*y14 ]
[ X1*y21 X2*y22 X3*y23 X4*y24 ]
...
[ X1*y21 X2*y22 X3*y23 X4*y24 ]
While VEE can easily multiply a scalar times a vector, and can perform matrix
multiplications, this operation was neither fish nor fowl. As noted, a
scalar multiplication multiplies every element in a matrix by a scalar to
give a result matrix of the same size as the original, while a matrix
multiplication is an operation between an MxN matrix and an NxM matrix that
yields a scalar.
[ 1 2 3 4 ]
-- and a matrix of the form:
[ 1 2 3 4 5 6 7 8 ]
[ 10 20 30 40 50 60 70 80 ]
[ 100 200 300 400 500 600 700 800 ]
[ 1000 2000 3000 4000 5000 6000 7000 8000 ]
The desired result was:
[ 1 2 3 4 5 6 7 8 ]
[ 20 40 60 80 100 120 140 160 ]
[ 300 600 900 12K 15K 18K 21K 24K ]
[ 4000 8000 12K 16K 20K 24K 28K 32K ]
The multiplication could be performed in a Formula box (see xarrvcml.vee
for a developed example):
+---------------------------------------------------------+
| Formula |
+---+-------------------------------------------------+---+
matrix -->| M | | |
| |[M[0,*]*V[0] M[1,*]*V[1] M[2,*]*V[2] M[3,*]*V[3]]| R +-->
vector -->| V | | |
+---+-------------------------------------------------+---+
Tests indicated that this was only about 50% slower than a scalar
multiplication of the same array.
+-------------------------------------------+
| UserFunction: InsData |
+----------+----------------------------+---+
| | | |
| N +--+ | |
| | | +---+------------+ | |
| | +-->| A | | | |
| NewData +----->| B | Formula +--->| X |
| | +-->| C | | | |
| | | +---+------------+ | |
| OldArray +--+ | |
| | | |
+----------+----------------------------+---+
The Formula contains (expanded to a multiline format for clarity):
( A <= 0 ?
concat( B, C ) :
( a >= totSize(C) ?
concat ( C, B ) :
concat ( C[0:(A-1)], concat( B, C[A:(totSize(C)-1)] ) )
)
)
You give the UserFunction an index value N to indicate what the starting
index of the new data should be. If N is 0 or less, the new data is
concatenated to the start of the UserFunction. If N is the length of the
array of old data or greater, the new data is concatenated to the end of the
array. If it is some value in between, the old array is broken in segments
around the index value and the new data is spliced into it.
+--------------------------------------+
| |
+-->+-----+ |
+---------------+ | JCT +--+ |
| ramp(10,0,0) +----->+-----+ | +------------------+ |
+-------+-------+ | | Call Function | |
| | +---+--------------+ |
+-----+-----+ +-->| A | | |
| On | | |[ Queue ]+--+-->
| Cycle: 1 +---+ +-->| B | |
+-----------+ | | +---+--------------+
| |
+----+----+ |
| Random | |
| Number +--------+
+---------+
The program simple allocates an initial empty array of ten elements to act as
a queue. It then goes into a loop, adding a random numbers to the head of
the queue every second. The UserFunction "Queue" takes the last nine
elements of the input array (on pin "A") and concatenates it with a new
random number (on pin "B"), using the Formula:
concat( A[1:(totSize(A)-1)],B )
* This assumes that you are adding values one at a time, however, and that
the size of the initial array pushed into the Formula will define the length
of the queue from then on.
+---+----------------------------------------------------------+---+
| | | |
| | +---------------------------+ | |
| | | Formula | | |
| | +-----+---------------------+ | |
| A +----->| Old |[ concat(Old, New) ] +--+ | |
| | +-->| New | | | | |
| | | +-----+---------------------+ | | |
| | | | | |
| | | +----------------+----------+ | |
| | | | | | |
| | | | | +-----------------------+ | |
| | | | | | Formula | | |
| | | | | +---------+-------------+ | |
| | | | +---------+ +-->| Queue | | Result +-->| X |
| | | +-->| totSize +----->| Size | | | | |
| | | +---------+ +-->| MaxSize | | | | |
| | | | +---------+----+--------+ | |
| B +--+ +---------+ | see below | |
| | | Integer | | | |
| | +---------+ | | |
| | | 20 +--+ | |
| | +---------+ | |
| | | |
+---+----------------------------------------------------------+---+
The Formula on the output truncates the queue with the expression:
(Size <= MaxSize ? Queue : Queue[Size-MaxSize:Size-1])
The queue length is given by the Integer constant, but you could also put it
in a Global constant if you wanted to set it elsewhere.
+------------------------------------+
| If/Then/Else |
+---+------------------------+-------+
array -->| A |[ product(A - B) == 0 ] | True |
test value -->| B | | False |
+---+------------------------+-------+
The idea is that if there is any value in array A that exactly equals the
test value B, subtracting A - B will give an array with a zero in it, and
taking the product of all those elements in the array will give a zero result
as well.
+-------------------------------------------------+
| If/Then/Else |
+---+---------------------------------------------+
array -->| A |[ (product(A - B) == 0 ? "True" : "False") ] +-->
test value -->| B | |
+---+---------------------------------------------+
* For a final example in this section, here's one that is extremely devious
and leverages off the operation of transaction objects, rather than array
functions as such.
+--------------------------------+
| From String |
+-----------------+ +----------------------------+---+
| ramp( 9, 0, 8 ) +-->| READ TEXT x REAL ARRAY:*,3 | x +-->
+-----------------+ +----------------------------+---+
This takes a vector of the format:
0,1,2,3,4,5,6,7,8
-- and converts it to a matrix of the format:
0,1,2
3,4,5
6,7,8
If you don't like this row-ordered format, you can convert it to a
column-ordered format with the "transpose()" (matrix transpose) function:
0,3,6
1,4,7
2,5,8
VEE's automatic data conversion ability makes this possible.
[4.3] ADVANCED ARRAY OPERATIONS
+-----------+ +-------------+
| Noise | | Set Global |
| Generator +--->+-------------+
+-----------+ |[ sumdata ]|
+------+------+
|
+-----------------+ +------+ OK Button
| | Quit +--+
+----+----+ +------+ |
| Until | |
| Break +--+ +---+---+
+---------+ | | Stop |
| +-------+
+-----+-----+
| Get Data +--+
+-----------+ |
OK Button | +---------+ +------------+
+-----+-----+ | Formula | | Set Global |
| Noise | +---+-+---+ +------------+
| Generator +------>| A | | R +-->|[ sumdata ]|
+-----+-----+ +-->| B | | | +------------+
| | +---+-+---+
+------+------+ |
| Get Global | |
+-------------+ |
|[ sumdata ]+--+
+-------------+
This program simulates input data by using a Noise Generator. It gets an
initial data set and puts it in a global variable named "sumdata", then goes
into a loop where to obtain new data.
clipLower(A, B)
This gives a result array with the value of A if A > B, and the value of B
otherwise. If you used "clipUpper" instead, you would obtain minimum values.
+-------+
| Label |
+---+---+ +--------------+
| | Timer |
+------------------------------->+--------------+
| | |
+-------+-------+ +-->+--------------+
| randomSeed() | |
+-------+-------+ |
| | +--------------+
+-------+-------+ | | AlphaNumeric |
| randomize() +------>+---------+ | +--------------+
+-------+-------+ | Formula +--|-->| 510 |
| +--->+----+----+ | +--------------+
+-------+-------+ | | |
| randomize() +--+ +-------+
+---------------+
The "randomSeed()" function seeds the random-number generator with a seed
that varies rapidly with time:
randomSeed((10^9)*fracPart(now()/100))
This ensures that the data varies between different runs of the program. The
two "randomize()" functions each contain the expression:
randomize(ramp(1000,0,999), 0, 1)
This generates an array of 1000 random numbers in the range 0 to 1. Finally,
the summation is performed by the following expression in the Formula box:
sum(clipLower(0,signof(A-B)))
You might think at first that you could simply use the relational operators
("A > B") or the triadic operator ("( A > B? 1 : 0 )") to do this, but
although they don't give an error message when provided with arrays as
parameters, they only give a scalar result. So you have to "fake" the
relational operations by other means that yield an array result. Let's see
how this is done by unraveling the expression above part-by-part:
A == B: (1-abs(signof(A-B)))
A != B: abs(signof(A-B))
A > B: clipLower(0,signof(A-B))
A < B: clipLower(0,-signof(A-B))
A >= B: (1-clipLower(0,-signof(A-B)))
A <= B: (1-clipLower(0,signof(A-B)))
Note how subtracting an array of 1s and 0s from 1 performs a NOT operation on
the array. Similar tricks can be used for comparison with scalar values,
rather than other arrays. You can also perform Boolean operations on the
resulting arrays of 1s and 0s. For example, suppose that A1 and A2 are two
such arrays; then the following logic operations hold:
NOT A1: 1 - A1
A1 AND A2: A1 * A2
A1 OR A2: signof(A1 + A2)
A1 XOR A2: 1 - abs(signof((A1 + A2) - 1))
You can use the results of these computations to perform "masking" on arrays
of the original values through multiplication. Those values that match to 0
are then wiped out, and those that match to 1 are retained.
(A + (clipLower(-1,clipUpper(0,A))) * (-256))
This expression performs the following operations:
0 0 0 0 1 1 0 0 1 1 0
-- what is the easiest way to find the indexes of the array elements where
the value makes a transition from 0 to 1, or the reverse?
+---------+
| Integer |
+---------+
| 0000: 0 |
| 0001: 0 | +-----------------------------------------------------+
| 0002: 0 | | Formula |
| 0003: 0 | +---+---------------------------------------------+---+
| 0004: 1 +-->| A |[ A[1:(totSize(A)-1)] - A[0:(totSize(A)-2)] ]| R +--+
| 0005: 1 | +---+---------------------------------------------+---+ |
| 0006: 0 | |
| 0007: 0 | +----------------------------------------+
| 0008: 1 | |
| 0009: 1 | | +-------------------------------+
| 0010: 0 | | | Comparator |
+---------+ +---+ | +------+-------------+----------+
| 0 +---|--->| Ref | | Passed |
+---+ | | | Test == Ref | Failed |
+--->| Test | | Failures +--+
+------+-------------+----------+ |
|
+---------------------------------------------------------------------+
|
| +----------------+
| +---------------+ +-----------------+ | Indexes |
| | UnBuild Coord | | Formula | +----------------+
| +-------+---+---+ +---+---------+---+ | 4 |
+-->| Coord | | X +-->| A |[ A + 1 ]| R +-->| 6 |
| Data | | Y | +---+---------+---+ | 8 |
+-------+---+---+ | 10 |
| |
+----------------+
The key to this program is the Formula box containing the formula:
A[1:(totSize(A)-1)] - A[0:(totSize(a)-2)]
To see how this works, add array indexes to the sample array listed at the
start of this discussion:
B0:0 1:0 2:0 3:0 4:1 5:1 6:0 7:0 8:1 9:1 10:0
^ ^ ^ ^
The array indexes where a transition occurs are marked. The formula above
basically performs a substraction of the input array from itself, staggered
by one index, to yield a new array as follows:
0:0 1:0 2:0 3:0 4:1 5:1 6:0 7:0 8:1 9:1 10:0
- 0:0 1:0 2:0 3:0 4:1 5:1 6:0 7:0 8:1 9:1 10:0
---------------------------------------------------------
0:0 1:0 2:0 3:1 4:0 5:-1 6:0 7:1 8:0 9:-1
^ ^ ^ ^
The Comparator then checks the result array to see which elements are not
equal to 0. Its "Failures" pin returns an array of X-Y coordinates giving
the index and value of the failure. Since we only care about the index, we
use Unbuild Coord to get rid of the Y values, and then increment the
resulting indexes by 1 to eliminate the effects of the staggered subtraction.