Iterating Statistical Driver Values: DriverSequence

Introduction

Instances of DriverSequence class generate sequences of double numbers by drawing values sequentially from a stored list. Values range from zero to unity.

The Sequence.UsageMode enumeration identifies four ways of using DriverSequence (and also four ways of using IndexSequence, but these have different implications).

Profile

Figure 1 illustrates four examples of DriverSequence output with sequences of 96 samples generated. All four examples were generated with 8 cycles, each with 12 samples. The random seed was 2, which produced a prettier shape for UsageMode.SAMPLE_DIRECT than seed value 1.


Figure 1: Sample output from DriverSequence.next() with different usage modes. The left graph in each row displays samples in time-series while the right graph in the same row presents a histogram analyzed from the same samples.

The vertical x axes for the two graphs in each row represent the driver domain from zero to unity; the horizontal k axis of the time-series graph (left) plots ordinal sequence numbers; the horizontal f(x) axis of the histogram (right) plots the relative concentration of samples at each point in the driver domain.

The topmost graph was generated with UsageMode.UNIQUE_DIRECT. The stored collection was populated with 12 values equally spaced between zero and unity. The first three cycles employed a sampling offset of 0 (start with leftmost stored value) and a sampling increment of 1 (advance rightward by single elements). The fourth cycle employed offset -1 (start with rightmost stored value) and increment of -1 (advance leftward by single elements). The fifth cycle reprised offset 0 and increment 1. The sixth and seventh cycle employed offset 5 (start with rightmost value in first half of collection) and increment -1 (advance leftward by single elements). The eighth and final cycle employed offset 6 (start with second half of collection) and increment 1 (advance rightward by single elements). The histogram on the far right was generated with a granualarity equal to the collection length. Since collection values are equally spaced, and since each stored value occurs a total of eight times during the overall sequence, the histogram flatlines.

The second-from graph was generated with UsageMode.UNIQUE_SHUFFLE. The stored collection was also populated with 12 values equally spaced between zero and unity; however this mode shuffles the order of these values at the beginning of each cycle. Although this presentation order changes, it remains true that each stored value occurs a total of eight times during the overall sequence. Hence the histogram flatlines for this sequence as well.

The third-from-top graph was generated with UsageMode.SAMPLE_DIRECT. Here an array containing the values {0, 0, 1, 2, 2, 3, 4, 5, 5, 6, 6, 7} was shuffled giving {5, 0, 2, 0, 3, 1, 7, 6, 6, 4, 5, 2}, then descaled to produce values equally spaced between zero and unity. The first three cycles employed a sampling offset of 0 (start with leftmost stored value) and a sampling increment of 1 (advance rightward by single elements). The fourth cycle employed offset -1 (start with rightmost stored value) and increment of -1 (advance leftward by single elements). The fifth cycle reprised offset 0 and increment 1. The sixth and seventh cycle employed offset 5 (start with rightmost value in first half of collection) and increment -1 (advance leftward by single elements). The eighth and final cycle employed offset 6 (start with second half of collection) and increment 1 (advance rightward by single elements). This time around the histogram granualarity remains at 12 but the number of distinct values has dropped to 8; thus, the histogram drops to zero in four places. Also, since values 0, 2, 5, and 6 have duplicates each recurs sixteen times during the overall sequence, while values 1, 3, 4, and 7 only recur eight times. The histogram is by no means flat.

The bottommost graph was generated with UsageMode.SAMPLE_SHUFFLE. Here the array containing the values {5, 0, 2, 0, 3, 1, 7, 6, 6, 4, 5, 2} was descaled to populate the stored collection; however this original ordering is never seen because this mode shuffles the order at the beginning of each cycle. In spite of the shuffling, it remains true that values 0, 2, 5, and 6 recur sixteen times, while values 1, 3, 4, and 7 only recur eight times. Thus the bottommost histogram exactly matches the third-from-top histogram.

Short-Term Uniformity

Output from DriverSequence, with UsageMode.UNIQUE_SHUFFLE selected, bears a particular relation to Lehmer, which wraps Java's standard random number generator. Output from both these drivers nominally follows a continuous uniform distribution; however of the two, only DriverSequence realizes this distribution over the short term. In that sense Lehmer is the continuous counterpart of the urn model's selection with replacement, just as DriverSequence is one continuous counterpart of selection without replacement.

This mode of output from DriverSequence is comparable to output from Balance. The difference is how these two drivers get to short-term uniformity. When the number of samples N making up a cycle or frame is known in advance, UNIQUE_SHUFFLE will produce a sequence of samples which is perfectly uniform, in the sense of having the flattest possible histogram. This is somewhat better than Balance can achieve — though only somewhat.

Balance employs a quasi-deterministic algorithm which actively seeks to place new samples in less-populated intervals. The nature of this algorithm is such that not only will an N-sample frame be very nearly uniform, both the first and second halves of the frame will also be very nearly uniform. By contrast, with DriverSequence in UsageMode.UNIQUE_SHUFFLE the halves of the frame will very likely not be uniform. For example, shuffling could place a preponderance of lower values in the first half, then compensate with a preponderance of higher values in the second half.

When employing DriverSequence with the UsageMode.UNIQUE_SHUFFLE option selected, one should consider offsetting each sample with a random adjustment chosen uniformly between −Δ/2 and +Δ/2, where Δ = 1/N.

Externally Obtained Samples

UsageMode.SAMPLES_DIRECT was devised to exploit externally obtained sequences, regardless of source. The exploitation of externally obtained sequences has happened in at least two musical compositions that I know of: "Earth's Magnetic Field" by Charles Dodge and "Canadian Coastlines" by Larry Austin.

The challenge posed by externally obtained data is that of adapting values whose source range has nothing to do with the internal purposes the values will be exploited for. Bringing this material into the Driver/Transform model of selection means first descaling the source values so that all elements in the descaled sequence have been shifted, squeezed, and/or stretched into the driver domain from zero to unity. By abstracting out the source range in this manner, a Transform can be employed to map these descaled values to, for example, the range of a performing instrument.

Let N be the number of source samples, so that X0, X1, X2, …, XN-1 represent the original source values.

Suppose the extremes of the target range are themselves legitimate choices. This stipulation makes it reasonable to map the lowest source value to zero in the driver domain, which in turn will map to the minimum range value; likewise it is also reasonable to map the highest source value to unity in the driver domain, which in turn will map to the maximum range value. Discovering the lowest source value Xlow and the highest source value Xhigh is easily accomplished by iterating Xj over j. Once done the descaled value Yj can be derived using the calculation:

Yj  = 
Xj - Xlow
Xhigh - Xlow

Suppose the extremes of the target range are not themselves legitimate choices. This happens for example with durational data where the minimum range value of 0 is not a valid duration and the range has no finite upper bound. A solution in this situation would be establish Δ = 1/N as padding. Here the lowest source value would map to 0 + Δ/2 in the driver domain, which in turn will map to higher than the range minimum; likewise the highest source value would map to 1 − Δ/2 in the driver domain, which in turn will map to lower than the range maximum. The calculation for the descaled value Yj then becomes:

Yj  = 
(1 − Δ)×(Xj - Xlow)
Xhigh - Xlow
 + Δ/2
/**
 * Instances of the {@link DriverSequence} class iterate through a stored
 * collection of real numbers between zero and unity.
 * @author Charles Ames
 */
public class DriverSequence
extends Sequence<Double> implements Driver {
   /**
    * Constructor for {@link DriverSequence} instances.
    * @param container A {@link WriteableEntity} which contains this driver.
    */
   public DriverSequence(WriteableEntity container) {
      super(container);
   }
   /**
    * Constructor for {@link Balance} instances without container.
    */
   public DriverSequence() {
      this(null);
   }
   @Override
   public void addValue(Double value) {
      Driver.checkDriverValue(value);
      super.addValue(value);
   }
   /**
    * Append an array of elements to {@link Sequence#values}, normalizing each
    * element so that the minimum value is zero and the maximum value is unity.
    * @param source The value array.
    * @param aysmptotic Adjust range to pad slightly above zero and below unity.
    * @throws UnsupportedOperationException
    * when {@link Sequence#values} already contains elements.
    * @throws UnsupportedOperationException
    * when iteration from values is in progress.
    */
    public void fillDescaledValues(Double source[], boolean aysmptotic) {
      if (0 != itemCount())
         throw new UnsupportedOperationException("Values already populated");
      double minValue = Double.MAX_VALUE;
      double maxValue = -Double.MAX_VALUE;
      for (double value : source) {
         minValue = Math.min(minValue, value);
         maxValue = Math.max(maxValue, value);
      }
      double range = maxValue - minValue;
      if (aysmptotic) {
         double delta = 1./source.length;
         double scale = 1. - delta;
         double offset = 0.5*delta;
         for (double value : source) {
            double v = (value-minValue)/range;
            addValue(offset + (scale*v));
         }
      }
      else {
         for (double value : source) {
            addValue((value-minValue)/range);
         }
      }
   }
   /**
    * Append a collection of elements to {@link Sequence#values}, normalizing each
    * element so that the minimum value is zero and the maximum value is unity.
    * @param source The value collection.
    * @param aysmptotic Adjust range to pad slightly above zero and below unity.
    * @throws UnsupportedOperationException
    * when {@link Sequence#values} already contains elements.
    * @throws UnsupportedOperationException
    * when iteration from {@link Sequence#values} is in progress.
    */
public void fillDescaledValues(List<Double> source, boolean aysmptotic) {
      if (0 != itemCount())
         throw new UnsupportedOperationException("Values already populated");
      double minValue = Double.MAX_VALUE;
      double maxValue = -Double.MAX_VALUE;
      for (Double value : source) {
         minValue = Math.min(minValue, value);
         maxValue = Math.max(maxValue, value);
      }
      double range = maxValue - minValue;
      if (aysmptotic) {
         double delta = 1./source.size();
         double scale = 1. - delta;
         double offset = 0.5*delta;
         for (Double value : source) {
            double v = (value-minValue)/range;
            addValue(offset + (scale*v));
         }
      }
      else {
         for (Double value : source) {
            addValue((value-minValue)/range);
         }
      }
   }
   @Override
   public void fillAscending(int itemCount) {
      if (0 >= itemCount)
         throw new IllegalArgumentException("Item count must be positive");
      if (0 != itemCount())
         throw new UnsupportedOperationException("Values already populated");
      double increment = 1. / itemCount;
      double value = 0.5 * increment;
      while (value < 1.0) {
         addValue(value);
         value += increment;
      }
   }
   /**
    * Fill the {@link Sequence#values} collection with itemCount driver values
    * which are generated by the indicated driver instance.
    * @param itemCount The intended length of the {@link Sequence#values} collection.
    * @param driver The driver instance which will generate the values.
    * @throws IllegalArgumentException when the itemCount argument is not positive.
    * @throws UnsupportedOperationException when iteration from {@link Sequence#values}
    * is in progress.
    */
   public void autoFillValues(int itemCount, Driver driver) {
      if (0 >= itemCount)
         throw new IllegalArgumentException("Size must be positive");
      if (0 != itemCount())
         throw new UnsupportedOperationException("Values already populated");
      for (int index = 0; index < itemCount; index++) {
         if (!driver.hasNext()) break;
         addValue(driver.next());
      }
   }
   @Override
   public double getValue() {
      throw new UnsupportedOperationException("Method not supported");
   }
   @Override
   public void setValue(double value) {
      throw new UnsupportedOperationException("Method not supported");
   }
   @Override
   public void randomizeValue() {
      throw new UnsupportedOperationException("Method not supported");
   }
   @Override
   protected void checkValue(Double value) {
      Driver.checkDriverValue(value);
   }
   @Override
   protected int compareValues(Number a, Number b) {
      double d = ((Double) a) - ((Double) b);
      if (d > MathMethods.TINY) return 1;
      if (d < -MathMethods.TINY) return -1;
      return 0;
   }
}
Listing 1: The DriverSequence implementation class.

Coding

The type hierarchy for DriverSequence is:

Most of the functionality in DriverSequence is actually implemented by the Sequence base class. The life cycle of a DriverSequence instance has three phases: Construction, Population, and Consumption:

Once consumption has commenced, it is bad practice to make further calls to addValue() without first calling clear(). The clear() method empties out the values collection, establishing a clean slate.

These methods assist in the Population phase:

These methods assist in the preparation stage of the Consumption phase:

© Charles Ames Page created: 2022-08-29 Last updated: 2022-08-30