Continuous Trapezoidal Transform1

Introduction

The ContinuousTrapezoidal transform adapts values from the driver domain to a bounded range, where the relative concentrations at each extreme of the range are specified, and the concentrations in between progress linearly.

The range of values output by ContinuousTrapezoidal.convert() is controlled by two parameters implemented as Java fields: minRange and maxRange. These have the restriction that minRange < maxRange.

The distribution of values between minRange and maxRange is controlled by two additional parameters: origin and goal. The restrictions upon origin and goal are these:

Each ContinuousTrapezoidal instance internally maintains a ContinuousDistribution instance with a single item. This item's left field is zero, the item's right field is unity, the item's origin field is equal to ContinuousTrapezoidal.origin, and the item's goal field is equal to ContinuousTrapezoidal.goal.

The convert() method maps a value x in the driver domain from zero to unity into a value v in the application-range from minRange to maxRange in two steps. The first step uses ContinuousDistribution.quantile() to recast the driver value x into an intermediate value z, also between zero and unity. The second step applies the linear interpolation formula:

v = (maxRange-minRange)*z + minRange.

Profile

Figure 1 illustrates the influence which ContinuousTrapezoidal.convert() exerts over driver sequences. This panel was created using the same driver sources used for the ContinuousUniform, which earlier panel provides a basis for comparison.


Figure 1: Panel of ContinuousTrapezoidal output from three different Driver sources. The weight associated with the upper range bound is three times the weight associated with the lower range bound. Each row of graphs provides a time-series graph of samples (left) and a histogram analyzed from the same samples (right). The first row of graphs was generated using the standard random number generator. The second row was generated using the balanced-bit generator. The third row was generated using an ascending sequence of driver values, equally spaced from zero to unity.

The standard-random time-series graph (top row of Figure 1) has the same relative ups and downs as the standard-random time-series graph prepared for ContinuousUniform, but the specific values are squinched up toward the upper range bound. This difference becomes much clearer in the standard-random histogram, where the whitespace separating the vertical v axis from the smallest f(v) value progressively increases as v increases from zero to unity. Notice that while these histogram peaks and valleys are similar to those derived for ContinuousUniform, they are not the same. The fact that values squinch upwards means that range values which fell into the bottommost histogram region in the uniform histogram were spread across the bottom three regions here in the trapezoidal histogram. Likewise the range values which fell into the topmost histogram region here were spread across three regions in the uniform histogram.

The balanced-bit time-series (middle row of Figure 1) likewise has the same ups and downs as the balanced-bit time-series graph prepared for ContinuousUniform with values squinched similarly. Since balanced-bit sequences strive aggressively for uniformity, the histogram peaks and valleys are comparatively restrained.

The time-series graph generated using ascending, equally spaced driver values (bottom row of Figure 1) presents the percentile function for this particular flavor of continuous trapezoidal distribution. The histogram of sample values presents the distribution's probability density function or PDF. The PDF is a straight line sloping upward from f(v) = 1 when v = 0 to f(v) = 3 when v = 1. Looking back at the time-series graph, notice how the percentile function rises more steeply where the distribution is rarefied and less steeply where the distribution is concentrated.

For each graph in Figure 1 the average sample value is plotted as a dashed green line, while the interval between ± one standard deviation around the average is filled in with a lighter green background. For the ideally uniform driver values plotted in the third row of graphs, the average sample value is 0.583 and the standard deviation is 0.289. The interval from 0.583-0.289 to 0.583+0.289 is 2*0.276 = 0.55 = 55% of the full application range from zero to unity. Since the continuous uniform distribution had 58% of samples within ± one standard deviation of the mean, this suggests that with the trapezoidal distribution with origin 1 and goal 3 is squeezing 58% of samples into 55% of the application range, giving a concentration rate of 58/55 = 1.05.

Coding

/**
 * The {@link ContinuousTrapezoidal} class implements a {@link Transform}
 * which redistributes driver values according to a straight-line
 * weighting function, then adapts the results from zero to unity to a
 * bounded range.
 * The minimum and maximum range bounds are managed by the
 * {@link BoundedTransform} superclass.  The distribution weighting is
 * controlled by the {@link origin} field, giving the weight as of the
 * minimum range bound, and the {@link goal} field, giving the weight
 * as of the maximum range bound.
 * @author Charles Ames
 */
public class ContinuousTrapezoidal extends BoundedTransform {
   /**
    * Determines weight as of leftmost bound.
    */
   private double origin;
   /**
    * Determines weight as of rightmost bound.
    */
   private double goal;
   /**
    * Determines whether a change in parameter values requires the
    * distribution to be recalculated.
    */
   private boolean valid;
   /**
    * Constructor for {@link ContinuousTrapezoidal} instances.
    * @param container An entity which contains this transform.
    */
   public ContinuousTrapezoidal(WriteableEntity container) {
      super(container);
      this.origin = Double.NaN;
      this.goal = Double.NaN;
      this.valid = false;
   }
   /**
    * Getter for {@link origin}.
    * @return The assigned {@link origin} value.
    * @throws UninitializedException when {@link origin} is not initialized.
    */
   public double getOrigin() {
      if (Double.isNaN(this.origin))
         throw new UninitializedException("Origin not initialized");
      return origin;
   }
   /**
    * Setter for {@link origin}.
    * @param origin The intended {@link origin} value.
    * @throws IllegalArgumentException when the origin is negative.
    * @throws IllegalArgumentException when both origin and goal would be zero.
    */
   public void setOrigin(double origin) {
      checkOrigin(origin);
      if (this.origin != origin) {
         this.origin = origin;
         this.valid = false;
      }
   }
   /**
    * Check if the indicated value is suitable for {@link origin}.
    * @param origin The indicated value.
    * @throws IllegalArgumentException when the origin is negative.
    * @throws IllegalArgumentException when both origin and goal would be zero.
    */
   public void checkOrigin(double origin) {
      if (0. > origin)
         throw new IllegalArgumentException("Negative origin");
      if (!Double.isNaN(this.goal)) {
         if (this.goal < MathMethods.TINY && origin < MathMethods.TINY)
            throw new IllegalArgumentException("Either origin or goal must be positive");
      }
   }
   /**
    * Getter for {@link #goal}.
    * @return The assigned {@link #goal} value.
    */
   public double getGoal() {
      return goal;
   }
   /**
    * Setter for {@link #goal}.
    * @param goal The intended {@link #goal} value.
    * @throws IllegalArgumentException when the goal is negative.
    * @throws IllegalArgumentException when both origin and goal would be zero.
    */
   public void setGoal(double goal) {
      checkGoal(goal);
      if (this.goal != goal) {
         this.goal = goal;
         this.valid = false;
      }
   }
   /**
    * Check if the indicated value is suitable for {@link #goal}.
    * @param goal The indicated value.
    * @throws IllegalArgumentException when the goal is negative.
    * @throws IllegalArgumentException when both origin and goal would be zero.
    */
   public void checkGoal(double goal) {
      if (0. > goal)
         throw new IllegalArgumentException("Negative goal");
      if (!Double.isNaN(this.origin)) {
         if (goal < MathMethods.TINY && this.origin < MathMethods.TINY)
            throw new IllegalArgumentException("Either origin or goal must be positive");
      }
   }
   @Override
   public Double convert(double driver) {
      if (!valid) {
         getDistribution().calculateTrapezoidal(getOrigin(), getGoal());
         valid = true;
      }
      return super.convert(driver);
   }
}
Listing 1: The ContinuousTrapezoidal implementation class.

The type hierarchy for ContinuousTrapezoidal is:

Class ContinuousDistributionTransform embeds a ContinuousDistribution instance capable of approximating most any continuous distribution as a succession of trapezoids. For this particular implementation, exactly one such trapezoid is required.

Each ContinuousDistribution trapezoid item has left, right, origin, and goal fields. Of the single item for ContinuousTrapezoidal, left is zero, right is unity, origin is set equal to ContinuousTrapezoidal.origin, and goal is set equal to ContinuousTrapezoidal.goal.

Notice that the trapezoid ranges from zero to unity, not minRange to maxRange. The trick with leveraging ContinuousDistribution instances is that the succession of trapezoids needs recalculating every time a parameter changes. Updating one single trapezoid item is not that big a deal, but more typically the number of will be 20 or more (my canned Normal distribution uses 200 trapezoids); also, the calculating formulas often include exponents. So it makes sense to abstract the range boundaries out of the distribution and to apply range scaling separately.

The distributing step of conversion happens in ContinuousDistributionTransform, where the convert() method does this:

return distribution.quantile(driver);

Range scaling happens in BoundedTransform, where the convert() method does this:

return interpolate(super.convert(driver));

And BoundedTransform.interpolate(factor) does this (ignoring pesky initialization checks):

return (maxRange-minRange)*factor + minRange;.

ContinuousTrapezoidal has a valid field to flag parameter changes. This field starts out false and reverts to false with every change to either origin or goal. Each call to ContinuousTrapezoidal.convert() begins by testing valid. If a parameter change has rendered the distribution invalid, convert() calls on the distribution to regenerate its single trapezoid item

Comments

  1. The present text is adapted from my Leonardo Music Journal article from 1991, "A Catalog of Statistical Distributions". The heading is "Trapezoidal", p. 62.

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