Transforms

Introduction

Java classes implementing the Transform interface provide the passive component of the Driver/Transform design. Transform instances serve two purposes. Their primary purpose is adaptive: converting from the driver domain (zero to unity) to whatever range suits the demands of a specific application. The secondary purpose is distributive: influencing how values concentrate within various regions of the application range.

The Transform interface encourages adherence to these purposes by establishing a mandatory convert() method. This method does the actual work of transforming driver-domain values into application-range values. While it is likely that these driver-domain values will be provided by Driver.next(), this is not a requirement. However calling convert() with an argument outside zero to unity will throw an IllegalArgumentException.

Alternatively, there are next() and hasNext() methods which tighten the transform's coupling to its driver. These method names emulate Java's Iterator interface, whose approach to presenting sequences does away with explicit indexing. It also exemplifies the decorator design pattern where the Transform relays data requests to a Driver instance, which latter plays the role of the "decorated object". The next() method employs convert() to do the "decorating".

Concerning the secondary, distributive purpose of Transform instances, there is something everyone needs to understand: Although a lot of trouble is taken to influence where in the range output values concentrate and where they disperse, these efforts are effective only to the extent that the driver values are uniform. Uniformity means that if you divide the driver domain into some number of equal-sized intervals, then each interval should contain approximately the same number of driver values. The standard random number generator is uniform over the long term, but I have demonstrated graphically elsewhere that sequence lengths as long as 32 are hardly uniform. The balanced-bit generator produces short term uniformity, but the resulting sequences jump around a lot. If the sequence length N is known in advance, a good way to obtain uniformity is to generate N values equally spaced between zero and unity, then somehow order the items — perhaps by random shuffling or perhaps by deterministic serial techniques. Pretty much every other Driver implementation described on this site produce sequences which are less uniform than standard-random sequences, balanced-bit sequences, or equal-increment sequences.

Application ranges can be discrete or continuous. They can be bounded or unbounded. A range, combined with information concerning how values concentrate within the range, makes a statistical distribution. I am here deliberately using the term "statistical distribution" rather than probability distribution even though Wikipedia redirects the former term to the latter. My point in drawing this distinction is that in the context of distributions, the term "probability" specifically indicates a population density expressed as a proportion of the total population. Chance doesn't enter into the distribution concept. Remember that the Transform is the passive component of a Driver/Transform coupling. If chance is involved, the involvement must happen within the active Driver component.

The mathematical theory of distributions is well developed. In particular, knowing the probability density function (PDF) that describes how thickly values concentrate in each portion of the range, one can derive a quantile function which maps uniform driver values to application-range values, with the indicated concentrations. That is exactly what is needed to implement a Transform. And I have two distribution classes which take a numerically expressed PDF and give back quantile values: DiscreteDistribution when the application range consists of integer values, and ContinuousDistribution when the application range consists of real numbers. So it should not be surprising that most (but not all) of the Transform classes described on this site leverage one or the other of these two Distribution classes.

/**
 * Objects implementing the {@link Transform} interface share common methods
 * including {@link Transform#convert(double)}, which maps driver values
 * (double-precision values between zero and unity) into the range of the
 * transform.
 * @author Charles Ames
 * @param <T> The type of output from {@link Transform#convert(double)}.
 * Discrete transforms map driver values to {@link Integer} results.
 * Continuous transforms map driver values to {@link Double} results.
 */
public interface Transform<T extends Number> {
   /**
    * Transform the indicated driver value.
    * @param driver The indicated driver value.
    * @return The transformed result.
    */
   public T convert(double driver);
   /**
    * Call on the indicated {@link Driver} instance to select a value.
    * @param driver The indicated {@link Driver} instance.
    * @return The selected value.
    * @throws RuntimeException when the distribution has not been
    * normalized.
    */
   public T convert(Driver driver);
   /**
    * Transform a driver value drawn from the assigned {@link Driver} instance.
    * @param driver The indicated driver value.
    * @throws NoSuchObjectException When the driver field has not been assigned.
    * @return The transformed result.
    */
   public T next();
   /**
    * Set the source from which driver values will be drawn when
    * the {@link Transform#next()} method is called.
    * @param driver The intended source for driver values.
    */
   public void setDriver(Driver driver);
   /**
    * Get the source from which driver values are being drawn when
    * the {@link Transform#next()} method is called.
    * @throws NoSuchObjectException When the driver field has not been assigned.
    * @return The assigned source for driver values.
    */
   public Driver getDriver();
   /**
    * Get the lower range bound.
    * @return the lower range bound.
    */
   public T minRange();
   /**
    * Get the upper range bound.
    * @return the upper range bound.
    */
   public T maxRange();
   /**
    * Calculate a practical lower bound for graphing output from this
    * transform.
    * @param tail The combined weight of out-of-range values.  Ranges
    * from zero to unity, but smaller values are suggested.
    * @return A practical lower bound for graphing output from this
    * transform.
    * @throws IllegalArgumentException when the tail falls outside the
    * domain from zero to unity.
    */
   public T minGraphValue(double tail);
   /**
    * Calculate a practical upper bound for graphing output from this
    * transform.
    * @param tail The combined weight of out-of-range values.  Ranges
    * from zero to unity, but smaller values are suggested.
    * @return A practical upper bound for graphing output from this
    * transform.
    * @throws IllegalArgumentException when the tail falls outside the
    * range from zero to unity.
    */
   public T maxGraphValue(double tail);
   /**
    * The {@link DiscreteTransform} interface particularizes
    * the {@link Transform} interface to classes whose
    * {@link Transform#convert(Integer)} method outputs
    * double-precision numbers.
    * @author Charles Ames
    */
   public interface Discrete
   extends Transform<Integer>, Indexer {}
   /**
    * The {@link ContinuousTransform} interface particularizes
    * the {@link Transform} interface to classes whose
    * {@link Transform#convert(Double)} method outputs
    * double-precision numbers.
    * @author Charles Ames
    */
   public interface Continuous
   extends Transform<Double> {}
}
Listing 1: The Transforminterface.

Coding

Much of the functionality for my concrete Transform classes is implemented in base classes. The type hierarchy for Transform classes starts off with TransformBase. Immediate subclasses include DiscreteDistributionTransform and ContinuousDistributionTransform. DiscreteDistributionTransform embeds a DiscreteDistribution instance to manage value-weight mappings for discrete transforms. ContinuousDistributionTransform embeds a ContinuousDistribution instance to manage trapezoidal approximations to continuous probability curves.

The base classes take care of methods like convert(), hasNext(), and next(). Concrete Transform classes correspond to specific distribution formulas, and those formulas have specific parameters which need to be named, stored, and range-checked on a case-by-case basis. The concrete classes also have the job of monitoring parameter changes and of requesting recalculations in that event.

Interface

The Transform interface presented as Listing 1 enforces commonality of method calls.

Notice in Listing 1 that the Transform interface has a generic parameter T which extends java.lang.Number. Number is an abstract base class whose implementing sub-classes include Integer and Double. In practice, T can be either of these. Transform.Discrete (near bottom of Listing 1) particularizes Transform by setting T to Integer. Transform.Continuous (bottom of Listing 1) particularizes Transform by setting T to Double. Package consumers should reference the particularized interfaces in favor of the generic one. If the interface conventions had allowed me to make Transform protected while making Transform.Discrete and Transform.Continuous public, I would have done so.

 /**
 * The abstract {@link TransformBase} class implements base functionality
 * objects satisfying the {@link Transform} interface.
 * Many discrete {@link TransformBase} instances wrap
 * {@link DiscreteDistribution}, while many continuous
 * {@link TransformBase} instances wrap {@link ContinuousDistribution}.
 * @param <T> The type of value selected:
 * Discrete transforms map driver values to {@link Integer} results.
 * Continuous transforms map driver values to {@link Double} results.
 * @author Charles Ames
 */
public abstract class TransformBase<T extends Number>
extends WriteableEntity implements Transform<T> {
   private Driver driver;
   /**
    * Constructor for {@link TransformBase} instances with container.
    * @param container An entity which contains this transform.
    */
   public TransformBase(WriteableEntity container) {
      super(container);
      this.driver = null;
   }
   @Override
   public T convert(Driver driver) {
      return convert(driver.next());
   }
   @Override
   public final T next() {
      return convert(getDriver());
   }
   /**
    * Test if the sequence of values still contains values not yet presented.
    * @return true if unpresented values remain pending; false otherwise.
    * if the sequence is complete.
    * @throws {@link UnsupportedOperationException} when no driver has been assigned.
    */
   public final boolean hasNext() {
      Driver driver = getDriver();
      return driver.hasNext();
   }
   @Override
   public final Driver getDriver() {
      if (null == driver)
         throw new NoSuchObjectException("Driver not assigned");
      return driver;
   }
   @Override
   public void setDriver(Driver driver) {
      this.driver = driver;
   }
}
Listing 2: The TransformBase base class.

Implementation

The abstract TransformBase class presented as Listing 2 implements the shared decorator functionality of the next() and hasNext() methods for most units implementing the Transform interface. However the essential convert() method is left to subclasses DiscreteDistributionTransform and ContinuousDistributionTransform, whose embedded Distribution instances provide all important quantile() methods.

The TransformBase.next() implementation leverages convert() to 'decorate' output from the installed Driver instance's next() method. TransformBase.hasNext() simply delegates to Driver.hasNext(). Even though the driver field is accessible to all TransformBase methods, these methods instead rely on TransformBase.getDriver(), which performs an initialization check.

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