Data AcQuisition And Real-Time AnalysisScope - Spectrum - Spectrogram - Signal Generator
Software for Windows
Science with your Sound Card!
Contact us about
Random Macro Values
Macros: rand(), rnd(), Posn?R, Randomize
Macros can provide random values for test parameters, or random delays before the start of a test. These can be used, for example, to minimize subject guessing on behavioral tests, or to reduce possible unintended consequences of simply stepping linearly through a mechanical or electrical test sequence.
A numerical Daqarta control or variable can be assigned a random value within a given range using the rand(min,max) math function. The min and max terms can be constants, variables, or math expressions. The order of terms is not important, since Daqarta determines which is maximum and minimum.
For example, L.0.ToneFreq=rand(1000,2000) will set the Left Stream 0 Tone Frequency to a random value that is uniformly distributed between 1000 and 2000 Hz. It will appear to the control just as if you had entered it as a normal constant value. As with a constant entry, this will be subject to the current frequency entry Step Mode setting, so the actual frequency can be constrained to specific steps within the range, if desired.
Note: When you use a random value to set a parameter, it is often a good idea to save the actual value to a Field that will be saved with the test results. You can do that explicitly, or by first setting the desired Field to automatically monitor the target control before you set the control with the random value. Alternatively, you can append it to Notes, or add an entry in a log file.
The random generator used for rand() is similar to those used in Noise Waves. It is technically "pseudo-random", in that it creates what appear to be totally random values, but the exact sequence of values eventually repeats if invoked enough times. However, the complete cycle is 9.22 * 10^18 values, so even if you used 48000 values per second the sequence would not repeat for over 6 million years.
Because the random generator is pseudo-random, it has the sometimes-useful property that it can repeat the same sequence of values exactly on each future session, as long as the same macros are used in the same order.
But many times you will want completely random values from session to session. To insure this, you should randomize the generator before you use it the first time in a session. You do this with Randomize=1. This uses several unpredictable values (such as the number of CPU cycles since the system was started) to scramble the seed array in the random sequence generator. This is the same method used by the Timing Randomize function for Noise Waves in the Daqarta Generator.
Note that this does not make the values "more random". The normal random process is similar to reading "perfectly" random values from a circular list of 2^63 - 1 entries (9.2 * 10^18); randomizing just picks a different starting point in the list. In particular, it is usually pointless to randomize more than once per session.
As mentioned above, the rand() generator is really pseudo-random and can repeat the same sequence of values exactly on each future session, as long as the same macros are used in the same order. That may not always be convenient, but a bigger problem is that you can't repeat the sequence in the same session.
The rnd(min,max) math function is a simpler linear congruential generator (LCG) that not only allows you to repeat the sequence at any time, but can also be customized to create shorter sequences for special purposes. Internally, it creates 32-bit integers that can be returned via Posn?R instead of min,max scaled values.
A linear congruential generator has the form:
x[n] = (a * x[n-1] + c) mod 2^b
Here x[n] is the current random value, x[n-1] is the previous value or seed, a is a multiplier such that a mod 8 = 5, and c is an odd constant. b is the bit width of the random value, defaulting to 32. The generator produces a sequence of m = 2^b random values before repeating, covering all values in the range of 0 to m-1.
Violation of the above rules for setting the LCG parameters can result in "pathological" sequences that have short repeat lengths or obviously non-random sequences. The default (and highly recommended) value for a is 2891336453, the optimal value for a 32-bit LCG found by L'Ecuyer (see Reference below). The default c constant is 1.
You can use the Posn# Miscellaneous Position functions to set a, b and c, as well as the x[n-1] seed value.
Posn#A sets a multiplier Posn#B sets b bit width Posn#C sets c constant Posn#r sets x[n-1] seed
For shorter sequences, set a = 2^b - 3. For example, to get a "random" sequence of 8 values before repeating, set b = 3 and a = 5. Set the seed to any value in the 0-7 range. Leave c = 1, or set any odd value.
Table of short-sequence LCG parameters:
m b a 8 3 5 16 4 13 32 5 29 64 6 61 128 7 125 256 8 253 512 9 509 1024 10 1021
"Tables of Linear Congruential Generators of Different Sizes and Good Lattice Structure", by Pierre L'Ecuyer, Mathematics of Computation, volume 68, number 225, January 1999, pp 249-260.
You can use the above linear congruential generator to fill a macro array with 1024 random values, all in one command. Buf0="<r(A,B)" fills all elements of Buf0 with uniformly distributed random values between limits A and B, while Buf0="<rt(A,B)" fills with a triangular distribution.
See the Fill With Uniform Random Values and Fill With Triangular Random Values subtopics under Macro Array Copy/Swap Operations.
To randomly select a value from a list, enter the list values into a macro array (Buf0-Buf7), then use rand() or rnd() to select a value to be used as an index into the array. For example, if you want to set variable A to one of the first 100 values in Buf0, you could use A=Buf0[rand(0,99)]. Note that arrays can be saved and loaded from files.
Suppose you want to run a series of 100 tests, one at each of 100 predetermined frequencies, testing each condition exactly once but in a random order. The above random-index method won't work because some index values may appear more than once, and some may not appear at all.
One way to use each index only once is to shuffle the list, then just step through each index sequentially. We'll discuss two ways to do this: The classical Fisher-Yates Shuffle and the much easier to use Parallel Array Shuffle:
The Fisher-Yates shuffle requires one less random value than the number of elements. The basic idea is to pick a random integer UR in the range of index values from 0 to UN, and swap the value at the URth position with the UNth value. Repeat for decreasing values of UN down to 1.
UN=99 WHILE.UN=>0 UR=rand(0,UN) X=Buf0[UN] Buf0[UN]=Buf0[UR] Buf0[UR]=X UN=UN-1 WEND.
After this finishes, the original values at positions 0 to 99 are unchanged, but appear in random order. You can then use a simple loop that starts at index 0 and steps through the list. Later, you can re-apply the shuffle to run another test series with the same values but in a different order.
You can use this shuffle approach with paired or multiple values, such as 100 different frequency and level combinations. Rather than shuffling two separate arrays in parallel (using the same random index on each, so they stay paired), you can simply shuffle a single array that starts with values that are equal to their indices, from 0-99. (You can use Buf0="<=#" to fill an array with its index. See Macro Array Math Operations.)
After shuffling, step through this index array and use each value as a random index to retrieve values from separate frequency and level arrays that are never shuffled themselves. You can use the same shuffled-index array with any number of separate parameter arrays.
Since macro arrays hold 1024 values, you could hold 10 sets of parameters in a single array, organized so that elements 0-99 hold values for one parameter, 100-199 hold the second parameter, and so on. Then the random 0-99 index can be used with an offset to get each parameter in turn.
For example, suppose the first 100 elements of Buf0 hold the shuffled index values 0-99, and Buf1 holds the sets of parameters. Then step through the Buf0 array with index UJ = 0 to 99, reading its value UI at each step. The use UI as an index into the Buf1 array with offsets of 0, 100, 200, etc, to read the parameters:
UJ=0 WHILE.UJ=<100 UI=Buf0[UJ] L.0.ToneFreq=Buf1[UI] L.0.Level=Buf1[UI+100] L.1.ToneFreq=Buf1[UI+200] L.1.Level=Buf1[UI+300] ... UJ=UJ+1 WEND.
You can avoid the need for the shuffle if your test series length is a power of 2. To do this, use the rnd() random function after first setting its a and b parameters set so the linear conguential generator hits all the values from 0 to 2^b-1 exactly once before repeating. See the Table of short-sequence LCG parameters under Repeatable Random Values, above. To run another test with a different sequence, change the c parameter to a different odd value.
The simplest way to shuffle a list of values is to use two arrays. The list to be shuffled goes into one array, here assumed to be the first 100 elements of Buf0. A second array (here Buf1) is filled with random values, then sorted over the element range of interest. That sorted sequence is then applied back to the first array.
Buf1="<r(0,1M)" ;Fill with uniform random values Buf1="<s(0,99)" ;Sort the first 100 elements Buf0="<sX(0,99)" ;Transfer sort to list array
Note that it doesn't much matter what random value range you use for Buf1, as long as it's not so tiny that the resulting values can't be resolved; here we use 0 to 1 million (1M). In particular, the random value range is unrelated to the range of element indexes in the list to be sorted. However, the sort index range in the second step must exactly match the target list range, which is also used in the final step.
The idea is that the values in Buf1 start out in a random order, such that sorting them scrambles the original order. Then that scrambled order is applied to the target list in Buf0.
To randomly set a binary (on/off) value to 0 or 1 use cint(rand(0,1)). This will first find a random value between 0 and 1, then round it to exactly 0 or 1 depending on whether it is below or above 0.5. You can omit the cint() if you are setting an integer-only variable like U0-9 or UA-Z, because the rounding will happen automatically during the integer conversion.
Alternatively, you can make use of the logical AND operator && like this: A=rand(-1,1)&&1. The logical AND treats numbers of 0 or less as 0, and anything greater as 1. So this statement sets A to 0 or 1 with a 50% probability.
At first glance it may appear that there is a bias toward returning 0, since the range from -1 through 0 is larger than the range above that up to 1. This would indeed be a problem if the random generator returned only integers -1, 0, and 1. But rand() and rnd() return real values with 64-bit or 32-bit resolution. Even with 32-bit resolution, the bias is only 1 part in over 4 billion.
To control the probability, just add a bias to the rand() or rnd() operation. For example, if you want A to be 1 only 10% of the time, use A=rand(-90,10)&&1. The rand(-90,10) will return negative values 90% of the time and positive values only 10% of the time, while the &&1 will turn those negative values to 0s and the positive values to 1s. (You could use other values for the rand, such as rand(-9,1) or rand(-0.9,0.1), as long as they have the same proportions.)
Both rand() and rnd() (with default parameters) produce uniform distributions of random values, meaning all values within the specified range are equally likely. This is what the Daqarta Generator produces when the Wave type is set to White. A histogram shows a flat line if enough values are used.
But most real-world measurements are not uniform, instead being clustered around some mean value, with progressively less of the measured values falling farther from the mean. The typical distribution is a "bell-shaped curve", technically called a normal distribution by statisticians but commonly known as a Gaussian distribution. We'll use the latter term here to avoid confusion of "normal" with "ordinary", "usual", or "default". The Generator produces this type of distribution when Wave is set to Gauss.
You can create a Gaussian distribution by adding several uniform random values together to get each output value. Statisticians usually specify 12 or more random values per output, but this may be overkill depending on your application.
To see how this works, consider adding two uniform random values together, such as A=rand(-10,10) + rand(-10,10). A can thus range from -20 to +20, but it's not uniform. Note that there is only one way that it can hit +20, which is when both rand() calls return +10 simultaneously. But there are a lot of ways it can hit 0, such as -1 and +1, -2 and +2, -3.456 and +3.456, etc. It turns out that the distribution is a perfect triangle, with 0 at the peak and -20 and +20 at the ends of the base.
To see this for yourself, make sure Sgram, Spectrum, and Trigger are off and open the Waveform Averager dialog (thin unmarked button under the Averager button on the Daqarta toolbar). Select Histogram and set Frames Request to 256. Now open the Generator dialog and load the White4Gauss.GEN Generator setup. Click the Left Wave Controls button to see the setup details.
The default Wave is a single White noise source on Left Stream 0 (L.0.Stream), with Level set to 25%. Toggle the Averager button and the histogram will show a rectangular distribution extending over the central +/-25% of the output range.
Next, hit the  button at the top of the Generator Stream dialog to show the L.1.Stream settings. This is an identical 25% White source. Click Stream On and hit Averager again to see the triangular histogram whose tails extend to +/-50% of the output range.
Repeat for L.2.Stream and L.3.Stream and watch the triangular histogram become more rounded in the center and grow longer tails. With all four streams active, the histogram is a nice bell shape which is actually fairly close to Gaussian.
A true Gaussian has tails that extend to infinity in both directions, but that is neither practical nor a realistic model of real-world measured values. Adding more than 4 random values per sample has very little effect on the main shape, mostly just serving to extend the tails.
(You can't do this with the default Generator configuration since there are only 4 streams per channel. But you can use the Multi_Channel Output Controls to combine all 8 streams into a single channel. You can do this even if you only have a stereo sound card, since Windows will emulate a multi-channel card by summing streams together. Start in normal stereo mode with both Left and Right outputs active. For the 4 Left and 4 Right streams toggle each Stream to On, set Wave to White Noise, and set Level to 12.5% (one-eighth, since we are summing 8 streams that must not exceed 100% total). Then click the main Multi-Channel Outputs button to open the control dialog. Toggle only Front Left on, as well as Left Streams 0-3 and Right Streams 0-3, then toggle Multi-Channel Outputs On.)
The standard deviation of the distribution is a relative measure of how wide it is. You can reduce the deviation by reducing each of the Stream Levels. For example, if you cut each from 25% down to 12.5%, the histogram peak will be twice as tall but half as wide. The tails will no longer reach the ends of the histogram, but will only reach halfway. (A true Gaussian would always have some tail, no matter how far from the center, even if very small.)
You can't increase the deviation in this example since if you increase the Levels above 25% the sum could sometimes go beyond 100%. This would cause clipping, which would cause spikes in the histogram at either end.
With 12.5% Level on all streams the active distribution only uses 50% of the available range. That allows you to use the Stream Offset control (just below Level) to move the mean of the distribution up or down. Offset is simply a constant value added to every sample, so it doesn't matter which stream you adjust, as long as the total Offset is less than +/-50%; you can do all the adjusting on Stream 0 for convenience.
At +50% Offset the peak of the distribution will be at +50% of full-scale. To go higher, you'd need to reduce all the Levels to make room.
These examples use the Generator waveform, where +/-100% is equivalent to +/-32767 counts of the 16-bit output range. For your own use in macro calculations, you can use any convenient range.
In principle, you can build any arbitrary distribution from a weighted sum of uniform distributions of various sizes. In practice this is not easy to do. What's more, an arbitrary distribution may require a large number of random values for each sample.
For most work, however, you probably need only a reasonable approximation to the desired distribution. The Arb_Rand_Distr Arbitrary Random Distribution macro mini-app allows you to convert any arbitrary shape into a special array that can be used to create a matching random distribution, needing only a single random value per sample.
You specify the distribution via a file of 1024 values. You can use a plain .TXT file, or .WAV, .DQA. or .DAT formats, allowing you to use the Daqarta Generator to create waveforms that will be turned into distributions. Alternatively, the mini-app allows you to create Gaussian distributions with arbitrary mean and standard deviation set by sliders or edit controls.
The mini-app superimposes the histogram over the specified distribution, so you can verify the accuracy. It will be a near-perfect match except at the least-frequent incidences (the tails of a Gaussian, for example), where it drops off too suddenly to a very low level of histogram hits.
The computations that create the array are somewhat slow (a second or two on a slow system), but the array can then be saved to a file. Afterward, your own macro can load the array and use it to generate the specified distribution at high speed. You can use the same _Arb_Distr_Task macro that the mini-app uses, or use that as an example to create a customized version for your own application.
Note that the use of macros to enter random values is not, in general, the best way to produce randomly-varying Generator parameters for an ongoing signal. The reason is that when a Generator control is changed, it causes the Generator to restart so that the waveform will immediately have the properties indicated by the settings. This typically causes an audible click in the output signal. Instead, consider using one or more of the Generator noise sources as stream modulators to change the desired parameters.
However, under certain circumstances you may wish to use random macros while blocking Generator restarts using GenUpdate.
See also Macro Overview
Questions? Comments? Contact us!We respond to ALL inquiries, typically within 24 hrs.
Over 35 Years of Innovative Instrumentation
© Copyright 2007 - 2023 by Interstellar Research
All rights reserved