Discrete Transforms

Introduction

Classes implementing the Transform.Discrete interface adapt driver values to the integer range from zero upwards. Integers are discrete: if k is an integer then k+1 and k-1 are also integers but intervening values such as k+0.5 are not integers. Integers never have digits right of the decimal point.

Although integers in general can be both positive and negative, the discrete statistical distributions upon which Transform.Discrete classes are based consistently adopt zero as a lower bound. Also, Transform.Discrete output is very frequently an intermediate step on the way to selecting content from an array or a List, and these structures don't cope well with negative indexing. For those applications where a negative lower bound remains necessary, it is simple enough to incorporate an offset.

All discrete-transform names are prefixed with the word "Discrete"; the prefix was needed to distinguish DiscreteUniform and DiscreteWeighted from ContinuousUniform and ContinuousWeighted. I thought, well the others are discrete too.

Discrete-transform ranges always begin at zero; the upper limit can be bounded or unbounded. When the upper limit is unbounded, the weights associated with range values must tail away to zero.

Listed alphabetically, the discrete transforms are:

/**
 * The {@link DiscreteDistributionTransform} class implements a discrete
 * statistical transform based on a distribution of values from 0 (inclusive)
 * to N (exclusive) with non-uniform weights. The
 * {@link DiscreteDistributionTransform} length field stands in for the number of values N.
 * As a statistical transform, {@link DiscreteDistributionTransform} does not itself employ probability or randomness.
 * Instead it responds to an externally generated driver sequence which may or may not be random.
 * {@link DiscreteDistribution#quantile(double)} converts the driver value to an outcome.
 * @author Charles Ames
 */
public abstract class DiscreteDistributionTransform
extends TransformBase<Integer> implements Transform.Discrete {
   /**
    * Constructor for {@link DiscreteDistributionTransform} instances.
    * @param container An entity which contains this transform.
    */
   public DiscreteDistributionTransform(WriteableEntity container) {
      super(container);
   }
   @Override
   protected DiscreteDistribution createDistribution() {
      return new DiscreteDistribution();
   }
   @Override
   protected final DiscreteDistribution getDistribution() {
      return (DiscreteDistribution) super.getDistribution();
   }
   /**
    * Get the heaviest weight in the distribution.
    * @return The heaviest weight.
    * @throws IllegalArgumentException when the distribution has not been normalized.
    * @throws RuntimeException when the distribution has no peak-weight item.
    */
   public final double heaviestWeight() {
      DiscreteDistribution distribution = (DiscreteDistribution) getDistribution();
      return distribution.heaviestItem().getWeight();
   }
   @Override
   public Integer convert(double driver) {
      DiscreteDistribution distribution = (DiscreteDistribution) getDistribution();
      if (!distribution.isNormalized()) distribution.normalize();
      int index = distribution.quantile(driver);
      return index;
   }
   @Override
   public boolean hasReset() {
      return false;
   }
   @Override
   public void reset() {
      throw new UnsupportedOperationException("Reset not supported");
   }
   @Override
   public final Integer minGraphValue(double tail) {
      DiscreteDistribution distribution = (DiscreteDistribution) getDistribution();
      return distribution.minGraphValue(tail);
   }
   @Override
   public final Integer maxGraphValue(double tail) {
      DiscreteDistribution distribution = (DiscreteDistribution) getDistribution();
      return distribution.maxGraphValue(tail);
   }
   @Override
   public final double minGraphValue() {
      return minGraphValue(0.01);
   }
   @Override
   public final double maxGraphValue() {
      return maxGraphValue(0.01);
   }
   @Override
   public Integer minRange() {
      DiscreteDistribution distribution = (DiscreteDistribution) getDistribution();
      return distribution.minRange();
   }
   @Override
   public Integer maxRange() {
      DiscreteDistribution distribution = (DiscreteDistribution) getDistribution();
      return distribution.maxRange();
   }
   /**
    * Get the size of an array which would have one element
    * for each range value.
    * @return The maximum range value, plus 1.
    */
   public final int getLimit() {
      DiscreteDistribution distribution = (DiscreteDistribution) getDistribution();
      return distribution.getLimit();
   }
   /**
    * Get the weight associated with the indicated value.
    * @param value The indicated value.
    * @return The associated weight.
    */
   public final double itemWeight(int value) {
      DiscreteDistribution distribution = (DiscreteDistribution) getDistribution();
      return distribution.getItemWeight(value);
   }
   /**
    * Get the weights data.
    * @return An array of non-negative double-precision numbers.
    */
   public final double[] weightArray() {
      DiscreteDistribution distribution = (DiscreteDistribution) getDistribution();
      double[] result = new double[getLimit()];
      int index = 0;
      Iterator<DiscreteDistributionItem> iterator = distribution.iterateItems();
      while (iterator.hasNext()) {
         result[index++] = iterator.next().getWeight();
      }
      return result;
   }
   /**
    * Get the weights data.
    * @return An ordered collection of non-negative double-precision numbers.
    */
   public final List<Double> weightList() {
      DiscreteDistribution distribution = (DiscreteDistribution) getDistribution();
      List<Double> result = new ArrayList<Double>();
      Iterator<DiscreteDistributionItem> iterator = distribution.iterateItems();
      while (iterator.hasNext()) {
         result.add(iterator.next().getWeight());
      }
      return result;
   }
   /**
    * Set the weights data from an array.
    * @param weights An array of non-negative double-precision numbers.
    * @throws IllegalArgumentException when any weight is negative.
    */
   protected void addWeights(double[] weights) {
      DiscreteDistribution distribution = (DiscreteDistribution) getDistribution();
      distribution.initialize();
      for (int value = 0; value < weights.length; value++) {
         distribution.addItem(value, weights[value]);
      }
      distribution.normalize();
      makeDirty();
   }
   /**
    * Set the weights data from a collection.
    * @param weights A collection of non-negative double-precision numbers.
    * @throws IllegalArgumentException when any weight is negative.
    */
   protected void addWeights(List<Double> weights) {
      DiscreteDistribution distribution = (DiscreteDistribution) getDistribution();
      distribution.initialize();
      int value = 0;
      for (Double weight : weights) {
         distribution.addItem(value++, weight);
      }
      distribution.normalize();
   }
   /**
    * Get the weights data as space-delimited text.
    * @return A whitespace-delimited list of decimal numbers.
    */
   public String formatWeightsText() {
      DiscreteDistribution distribution = (DiscreteDistribution) getDistribution();
      StringBuilder builder = new StringBuilder();
      int index = 0;
      Iterator<DiscreteDistributionItem> iterator = distribution.iterateItems();
      while (iterator.hasNext()) {
         if (0 < index++) builder.append(' ');
         builder.append(Double.toString(iterator.next().getWeight()));
      }
      return builder.toString();
   }
}
Listing 1: The DiscreteDistributionTransform base class.

Coding

The code behind DiscreteDistributionTransform class is presented in Listing 1 The type hierarchy for DiscreteDistributionTransform is:

The abstract DiscreteDistributionTransform class presented as Listing 1 manages an embedded DiscreteDistribution instance. Most, but not all, of the classes implementing the Transform.Discrete interface subclass from DiscreteDistributionTransform.

The DiscreteDistributionTransform class stores its embedded DiscreteDistribution instance in the distribution field. Since one subclass makes no use of this class's functonality, the distribution field is populated upon the first getDistribution() call. Hence methods that reference the distribution field must perform their own initialization checks.

For each integer value in the application range, the DiscreteDistribution instance maintains an item detailing a weight (unnormalized probability) and the cumulative sum of weights up to and including the present value. The weights define the distribution, while the cumulative sums support DiscreteDistribution.quantile().

Of the methods implemented in the class, convert() does the actual work of transforming driver-domain values into application-range values.

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