Drivers

Introduction

Sequence generators implementing the Driver interface provide the active component of the Driver/Transform design. To satisfy Driver, a generator must implement a next() method returning Double values ranging continuously from zero to unity. Sometimes the passive Transfer component seeks not just to conform the output of the Driver/Transform coupling just to a range, but also to a distribution. Under this circumstance, values from next() should additionally be distributed uniformly. However this circumstance does not always apply, so uniformity is not enforced by the Driver interface. (Which would not in fact be easy to do.)

The Transform interface encourages adherence to these purposes by establishing mandatory next() and hasNext() methods. These method names inherit from the java.lang.Iterator interface, whose approach to presenting sequences does away with explicit indexing.

The prototype for my Driver interface is the standard number generator implemented by Java as java.util.Random. Indeed my motivation for this abstraction was to develop a stable of sequence generators where Driver.next() could readily be swapped into applications where Random.nextDouble() might initially be considered.

The primary criterion for evaluating drivers is dependence. A sequence is said to be dependent when the sequence is in some sense redundant or predictable. All but one of the Driver instances described on this site are dependent in some way or another; the exception is the Lehmer driver, which wraps java.util.Random. I have taken great pains elsewhere to work out how to verify numerically that sequences produced by Random.nextDouble() are independent, which result directly concerns Lehmer. In theory this same test should fail for every other Driver implementation; we'll see if this actually happens.

The Driver units which I have implemented divide into three categories: deterministic, probabalistic, and chaotic.

Deterministic Drivers

Of deterministic Driver units there is just one: DriverSequence. DriverSequence presents successive values from a stored array. If the values in the stored array are uniform, then DriverSequence.next() produces ideally uniform sequences over frames with that number of samples. There are various ways to go about this. The most direct is to fill the array with values that are equally spaced between zero and unity, then somehow permute the stored values into non-ascending order.

The flavor of dependence exhibited by DriverSequence is exclusion: the fact that something happened recently supports the prediction that the same thing will not happen again soon.

The DriverSequence.hasNext() methods returns true until the stored array is exhausted, then false. A call to DriverSequence.reset() is required to restart the cycle.

Probabalistic Drivers

Of probabalistic Driver units there are Lehmer, Balance, Moderate, Borel, Brownian, Bolt, Rosy, and Voss.

Lehmer wraps Java's standard random number generator to produce values which are probablistically independent.

Balance produces sequences which are balanced in this sense: all subsets (even overlapping ones) are as uniformly distributed between zero and unity as that many samples can be. This is another flavor of exclusion dependence; Balance actively strives to locate samples in underrepresented regions, which is the converse of avoiding overrepresented regions.

Moderate, Borel, Brownian, and Rosy, and Voss, each implement flavors of proximity dependence.

All probabilistic drivers with the exception of Bolt have their hasNext() methods hard-coded to return true. Each call to Bolt.reset() fills a stored array with a Brownian sequence from an indicated origin to an indicated goal (both driver-domain values) over an indicated sequence length. The Bolt.hasNext() methods returns true until the stored array is exhausted, then false.

Proximity Dependence

From most probabilistic drivers there is a tendency for consecutive samples to follow close upon one another. How closely they follow is something that can be measured numerically, and this measure of proximity is one concrete way of distinguishing Driver behaviors.

Given a sequence X0, X1, X2, …, XN, then we want to measure how close the Xjs are to their Xj-1s. This quantity can be calculated using one of two methods.

The absolute value method computes a simple average. It calculates the difference between each pair of consecutive samples, takes the absolute value of each difference, sums up the absolute values, then divides this sum by N-1 (the number of pairs):

1

N-1
ΣN-1 j=0 |Xj+1 - Xj|

The root-mean-square (RMS) method computes a statistical deviation. It squares the difference between each pair of consecutive samples, sums up the squares, divides this sum by N-1, then takes the square root of the result:


1

N-1
ΣN-1 j=0 (Xj+1 - Xj)2

Opinions differ upon which method is preferable under which circumstances, but it seems to come down to the issue of outliers. Outliers are individual values which stray more than normal from the calculated norm. The RMS calculation is more sensitive to outliers. Since the present site celebrates outliers, it favors the RMS calculation over the absolute value calculation.

Driver
Class
100
Samples
1000
Samples
10000
Samples
100000
Samples
Lehmer0.4350.4330.4100.409
Balance0.4850.4830.4820.483
Moderate0.2540.2340.2340.235
Borel0.3430.3480.3530.353
Table 3: RMS distance between consecutive samples for probabilistic driver units.

Table 3 lists sample-to-sample proximities for the Lehmer, Balance, Moderate, and Borel drivers.

The Brownian driver has a deviation parameter which directly controls the average distance between consecutive samples. When the deviation is smaller than 0.25 and the containmentMode is set to REFLECT, then the deviation correlates very closely to calculated RMS proximity measure. Similar considerations apply to the Bolt driver.

The Voss driver has a depth parameter which directly controls the number of sample-and-hold streams. As the number of streams increases, values cluster in an ever-tightening bell curve focused half-way beween zero and unity.

The Rosy driver have retention and rolloff parameters which controls how long the most significant bits hold on to their set/clear states before reselecting. When retention is near its lower limit of zero, the Rosy driver produces standard randomness. As retention increases toward its upper limit of unity,

Chaotic Drivers

Of chaotic Driver units there are Logistic and Baker. These units implement chaotic algorithms to produce patterned sequences of variable complexity. Chaotic sequences generated by Logistic and Baker are decidely nonuniform. They tend rather to points of extreme concentration (so-called attractors), separated by rarefied gaps.

The flavor of dependence exhibited by DriverSequence is patterning.

Both chaotic drivers have their hasNext() methods hard-coded to return true.

/**
 * Objects implementing the {@link Driver} interface share common
 * methods including {@link Driver#next()}, which generates
 * double-precision values between zero and unity.
 * @author Charles Ames
 */
public interface Driver extends Iterator<Double> {
   /**
    * Items in the {@link ContainmentMode} enumeration prescribe how
    * driver values should be treated when they threaten to fall outside
    * the range from zero to unity.
    * This enumeration is used by the {@link Brownian} and {@link Bolt}
    * implementations of the {@link Driver} interface.
    * @author Charles Ames
    */
   enum ContainmentMode {
      /**
       * Wrap out-of-range values back into the range from zero to unity.
       */
      WRAP("Wrap out-of-range values back into the range from zero to unity") {
         @Override
         public double contain(double value) {
            return MathMethods.modulo(value, 1.);
         }
      },
      /**
       * Reflect out-of-range values off of the boundaries at zero and unity.
       */
      REFLECT("Reflect out-of-range values off of the boundaries at zero and unity") {
         @Override
         public double contain(double value) {
            return MathMethods.reflect(value, 1.);
         }
      };
      /**
       * Text description of this option.
       */
      private String text;
      private ContainmentMode(String text) {
         this.text = text;
      }
      /**
       * Getter for {@link #text}.
       * Use this method to generate option menu texts.
       * @return A text description of this option.
       */
      public String getText() {
         return text;
      }
      /**
       * If the indicated value falls outside the driver range (from zero to
       * unity), take steps to map the value back into the driver range.
       * @param value The indicated value.
       * @return A double-precision value in the range from zero to unity.
       */
      public abstract double contain(double value);
      /**
       * Identify a {@link ContainmentMode} instance from its description.
       * @param text The {@link ContainmentMode} description.
       * @return A {@link ContainmentMode} instance.
       */
      public static ContainmentMode valueFromText(String text) {
         for(ContainmentMode mode : ContainmentMode.values()) {
            if (mode.text.equals(text)) return mode;
         }
         throw new IllegalArgumentException(
            "There is no containment mode with text [" + text + "]");
      }
   }
   /**
    * Test that the indicated driver value falls in the range from zero
    * (inclusive) to unity (inclusive).
    * @param driver The indicated driver value.
    * @return True if the value is in the driver range; false otherwise.
    */
   static boolean valueInRange(double driver) {
      return (0. <= driver && 1. >= driver);
   }
   /**
    * Check that the indicated driver value falls in the range from zero
    * (inclusive) to unity (inclusive).
    * @param driver The indicated driver value.
    * @throws IllegalArgumentException when the value falls outside
    * the range from zero to unity.
    */
   static void checkDriverValue(double driver) {
      if (!valueInRange(driver))
         throw new IllegalArgumentException("Driver value [" + driver
            + "] outside range from zero (inclusive) to unity (exclusive)");
   }
   /**
    * Query if this driver class supports the {@link #reset()} operation.
    * @return true if {@link #reset()} is supported; false otherwise.
    */
   boolean hasReset();
   /**
    * Reset the sequence of values to the first.
    * @throws UnsupportedOperationException when the driver
    * class does not support this operation.
    */
   public void reset();
   /**
    * Getter for current sequence value.
    * @return The assigned value.
    */
   double getValue();
   /**
    * Setter for current sequence value.
    * @param value The intended value.
    */
   void setValue(double value);
   /**
    * Randomize current sequence value.
    */
   void randomizeValue();
}
Listing 1: The Driver interface.
/**
 * Miscellaneous Math methods.
 * @author Charles Ames
 */
public abstract class MathMethods {
   
   /**
    * Calculate a modulo b using the actual rules of modular arithmetic.
    * @param a The argument
    * @param b The modulus
    * @return The remainder
    */
   public static double modulo(double a, double b) {
      int q = (int) (a / b);
      double r = a - (b * q);
      double result;
      if (0 <= r) {
         result = r;
      }
      else {
         result = b + r;
      }
      return result;
   }
   /**
    * Reflect a into the range from 0 to b.
    * @param a The argument
    * @param b The modulus
    * @return The remainder
    */
   public static double reflect(double a, double b) {
      int q = (int) (a / b);
      double r = a - (b * q);
      if (0 > r) r = -r;
      double result;
      if (even(q)) {
         result = r;
      }
      else {
         result = b - r;
      }
      //logger.info("== reflect a " + a + " b " + b + " q " + q + " r " + r + " result " + result);
      return result;
   }
   
}
Listing 2: Excerpt from the MathMethods static helper-method class.

Coding

Interface

Up until very recently I had thought that Java interfaces specified methods shared between classes implementing the interface, but nothing else, certainly not code. Recently I discovered that the Eclipse IDE accepts static variables, static methods, and also enumerations. This makes it possible not just to say what methods are available, but to provide code support for range checks and other conformance issues.

The Driver interface presented as Listing 1 leverages java.lang.Iterator to define the all-important next() method (also hasNext()). It also provides boolean valueInRange() and exception-throwing checkDriverValue() methods to enforce holding driver values to the range from zero to unity. The ContainmentMode enumeration provides options for bringing sequences back into the driver domain when they threaten to wander out. Finally, the interface provides access methods to a value property.

Package consumers can prescribe the first sequence element by calling setValue(double) prior to the first call to next(). The value property can also be randomized by calling randomizeValue(). Access to the current value (which changes with each call to next()) is provided by the getValue() method.

Let u be a driver value, that is, a double between zero and unity. Then the value property imposes this contract upon the implementing classes: immediately after invoking setValue(u), a call to next() will return u. Expressed another way, let

X0, X0, …, XN

be the sequence of values generated by next(). Then Xk will be the value returned by getValue() prior to the kth call to next(). The kth call to next() will also return Xk.

/**
 * The abstract {@link DriverBase} class defines base functionality for
 * objects with a {@link DriverBase#next()} method that generates values
 * between zero and unity.
 * @author Charles Ames
 */
public abstract class DriverBase extends WriteableEntity implements Driver {
   /**
    * Shared singleton instance of Java's {@link Random} class.
    */
   private Random random;
   /**
    * Sequence value which will be returned by upcoming call to
    * {@link #next()}.
    */
   private double value;
   /**
    * Constructor for {@link DriverBase} instances with container.
    * @param container A {@link WriteableEntity} which contains this
    * driver.
    */
   public DriverBase(WriteableEntity container) {
      super(container);
      setIDQuality(AttributeQuality.MODIFIABLE);
      setNameQuality(AttributeQuality.MODIFIABLE);
      this.random = SharedRandomGenerator.getSingleton().getRandom();
      randomizeValue();
   }
   /**
    * Getter for {@link #random}.
    * @return The shared {@link java.util.Random} instance.
    */
   public Random getRandom() {
      return random;
   }
   /**
    * Getter for {@link #value}.
    * @return The assigned {@link #value}.
    */
   @Override
   public final double getValue() {
      return value;
   }
   /**
    * Setter for {@link #value}.
    * @param value The intended {@link #value}.
    * @throws IllegalArgumentException when the argument falls outside the range from zero to unity.
    */
   @Override
   public final void setValue(double value) {
      Driver.checkDriverValue(value);
      this.value = value;
   }
   /**
    * Randomize {@link #value}.
    */
   @Override
   public final void randomizeValue() {
      value = random.nextDouble();
   }
   @Override
   public final Double next() {
      double oldValue = value;
      value = generate();
      return oldValue;
   }
   @Override
   public boolean hasNext() {
      return true;
   }
   /**
     * Non-public method where the guts of the driver mechanism are implemented.
    */
   protected abstract double generate();
}
Listing 3: The DriverBase base class.

Implementation

The abstract DriverBase class presented as Listing 3 encodes shared functionality for most units implementing the Driver interface.

The setValue(), getValue(), and randomizeValue() methods prescribed by the Driver interface are, here in class DriverBase, directed to a value instance variable.

The contract which requires that whatever the package consumer puts into setValue() comes back immediately from next(), is enforced within next() by delegating sample generation to a generate() method, which specific subclasses must implement. Notice that generate() is not present in the public-facing Driver interface, but rather held protected by DriverBase so that only DriverBase and its subclasses are aware of the method.

Three actions are undertaken by next():

Notice that next() is designated final. This prevents the implementing class from supplying a next() override which countermands the contracted behavior.

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