Articulated Proximity: Rosy1

Introduction

The Rosy driver implements an algorithm that decides for each bit whether to retain the set/clear state from the previous sample value or to toss a coin. The decision is effected by a random trial based on a retention rate (proportion of retained versus reselected bits).

Other drivers concerned with distances between consecutive samples include the Brownian driver and with others which are obviated by Brownian.

The backstory behind the Rosy driver is embarrasing. My earlier "Catalog of Sequence Generators"1 wrongly presented this as an algorithm for 1/f noise, also known as "pink" noise. The source of that error (not trying to shift blame, just to document provinence) was a pre-publication draft of Computer Music, 1st edition, by Charles Dodge and Thomas Jerse. Trying to reconstruct the algorithm from my Catalog's pseudo-code was proving difficult, so I pulled out my copy of "Computer Music", now in its 2nd edition.2 And found something called the Voss algorithm, which was not the same at all. The rub is, just because the original algorithm doesn't generate proper 1/f noise doesn't mean the algorithm can't be useable as a driver. The name of the present Rosy class reflects this backstory. (It's not exactly pink.) How it works probably differs from the prepublication Dodge & Jerse code, which I no longer have.

Profile

Figure 1 (a) illustrates five examples of Rosy output with a sequence of 200 samples generated. All five examples were generated using a random seed of 1, an initial value of 0.5, and a rolloff parameter of 1.5. The only difference is the retention parameter, which varies as indicated.


Figure 1: Sample output from Rosy.next() with representative retention settings. 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 most characteristic of the sequences in Figure 1 is the first, which has samples clustering in various regions of the driver domain. As the retention parameter dials down to zero, the behavior comes to resemble uniform randomness (i.e., that produced by Lehmer).

Bitwise Analysis

Figure 2 takes the sequence shown in Figure 1 and breaks out what happens in bit 1 (zero or one-half), bit 2 (zero or one-quarter), bit 3 (zero or one-eighth), bit 4 (zero or one-sixteenth), and the residual bits (continuous between zero and one-sixteenth).


Figure 2: Bitwise analysis of a sequence generated by Rosy.next().

The bit-specific graphs in Figure 2 transition back and forth between a set state (bit value 1) and a clear state (bit value 0). Table 1 statistically analyses of sample the actual stats for these bit-specific graphs. By comparison with the equivalent table for the Lehmer driver, probability has shifted away from single samples between transitions toward multiple samples between transitions.

Transitions1 Sample2 Samples3 Samples4 Samples5 or more
Actual Bit 1128%91%
Actual Bit 25030%12%14%12%32%
Actual Bit 35932%16%16%6%27%
Actual Bit 48034%20%8%8%10%
Table 1: Sample counts between bit-specific set/clear state transitions.

Transitions

Figures 3 (a) through 3 (e) plot the range of sample-to-sample differences along the vertical Δx axis against the relative concentrations of these values along the horizontal fx) axis.


Figure 3 (a): Histogram of sample-to-sample differences from Rosy.next() with retention 0.90 after 10,000 consecutive samples.

Figure 3 (b): Histogram of sample-to-sample differences from Rosy.next() with retention 0.83 after 10,000 consecutive samples.

Figure 3 (c): Histogram of sample-to-sample differences from Rosy.next() with retention 0.71 after 10,000 consecutive samples.

Figure 3 (d): Histogram of sample-to-sample differences from Rosy.next() with retention 0.51 after 10,000 consecutive samples.

Figure 3 (e): Histogram of sample-to-sample differences from Rosy.next() with retention 0.16 after 10,000 consecutive samples.

Table 2 compares retention parameter settings with measured deviations for Δx around zero.

GraphParametric
Retention Setting
Actual
Sample-to-Sample Deviation
Figure 3 (a)0.900.176
Figure 3 (b)0.830.207
Figure 3 (c)0.710.239
Figure 3 (d)0.510.301
Figure 3 (e)0.160.378
Table 2: Standard deviation of Δx around zero.

Figure 4: Divergence of 4-nibble pattern counts from Rosy.next() with retention 0.90 and rolloff 1.5 after 10,000 samples per pattern.

Independence

Figure 4 presents a trend graph of histogram tallies for 4-nibble patterns generated using Rosy.next(). My analysis program decided to exclude low-frequency patterns by limiting the graph to the 8,192 largest tallies. The most frequent patterns were:

14141414
10101010
5555
1111

All of which had comparable tallies representing less than 1% presence. The distinguishing feature of these patterns is that they present the same value in sequence, which means that the output stayed within 1/16 of the driver domain for four consecutive samples. I don't believe there is anything special distinguishing these four patterns from any of the others that contributed to the bottom-most stair step in Figure 4.

The conclusion from Figure 4 is that the Rosy driver fails the 4-nibble independence test.

/**
 * Instances of the {@link Rosy} class generate a driver sequence in
 * which the most significant bits have a controllable likelihood to hold
 * over from the previous value.
 * @author Charles Ames
 */
public class Rosy extends DriverBase {
   /**
    * Number of bits manipulated, from most-significant downward.
    */
   private int depth;
   /**
    * Likelihood of most-significant bit retaining previous value.
    */
   private double retention;
   /**
    * Divisor for calculating retention rate for bit i+1 relative to
    * retention rate for bit i.
    */
   private double rolloff;
   private double scale;
   private int msb;
    /**
    * Constructor for {@link Rosy} instances with container.
    * @param container An entity which contains this driver.
    */
   public Rosy(WriteableEntity container) {
      super(container);
      this.retention = Double.NaN;
      this.depth = Integer.MIN_VALUE;
      this.rolloff = Double.NaN;
   }
   /**
    * Constructor for {@link Rosy} instances without container.
    */
   public Rosy() {
      this(null);
   }
   /**
    * Setter for {@link #depth}.
    * @param depth The intended {@link #depth}.
    */
   public void setDepth(int depth) {
      checkDepth(depth);
      if (this.depth != depth) {
         this.depth = depth;
         this.scale = Math.pow(2., depth);
         this.msb = ((int) scale) >> 1;
      }
   }
   /**
    * Check if the indicated value is suitable for {@link #depth}.
    * @param depth The indicated value;
    */
   public void checkDepth(int depth) {
      if (0 >= depth)
         throw new IllegalArgumentException("Depth not positive");
   }
   /**
    * Getter for {@link #depth}.
    * @return The assigned {@link #depth} value.
    */
   public int getDepth() {
      if (Integer.MIN_VALUE == depth) throw new UninitializedException("Depth not initialized");
      return depth;
   }
   /**
    * Getter for {@link #retention}.
    * @return The assigned {@link #retention} value.
    */
   public double getRetention() {
      if (Double.isNaN(retention)) throw new UninitializedException("Retention not initialized");
      return retention;
   }
   /**
    * Setter for {@link #retention}.
    * @param retention The intended {@link #retention} value.
    */
   public void setRetention(double retention) {
      checkRetention(retention);
      this.retention = retention;
   }
   /**
    * Check if the indicated value is appropriate for {@link #retention}.
    * @param retention The indicated value.
    */
   public void checkRetention(double retention) {
      if (retention < 0. || retention > 1.)
         throw new IllegalArgumentException("Retention rate outside range from zero to unity");
   }
   /**
    * Getter for {@link #rolloff}.
    * @return The assigned {@link #rolloff} value.
    */
   public double getRolloff() {
      if (Double.isNaN(rolloff)) throw new UninitializedException("Rolloff not initialized");
      return rolloff;
   }
   /**
    * Setter for {@link #rolloff}.
    * @param rolloff The intended {@link #rolloff} value.
    */
   public void setRolloff(double rolloff) {
      checkRolloff(rolloff);
      this.rolloff = rolloff;
   }
   /**
    * Check if the indicated value is appropriate for {@link #rolloff}.
    * @param rolloff The indicated value.
    */
   public void checkRolloff(double rolloff) {
      if (rolloff < MathMethods.ONE_PLUS)
         throw new IllegalArgumentException("Rolloff rate must be larger than unity");
   }
   @Override
   protected double generate() {
      Random random = getRandom();
      double result = getValue();
      double p = getRetention();
      double r = getRolloff();
      int limit = getDepth();
      int mask = msb; // must be valid if getDepth() worked.
      int oldValue = (int) Math.floor(scale * result);
      int newValue = 0;
      for (int level = 0; level < limit; level++) {
         int increment;
         if (random.nextDouble() < p) {
            increment = mask & oldValue;
         }
         else {
            increment = random.nextBoolean() ? mask : 0;
         }
         newValue += increment;
         mask >>= 1;
         p /= r;
      }
      return (newValue + random.nextDouble()) / scale;
   }
}
Listing 1: The Rosy implementation class.

Coding

The type hierarchy for Rosy is:

Listing 1 provides the source code for the Rosy class. The sequential process described at the top of this page is implemented by generate(), which is not public facing. Instead, generate() is called by DriverBase.next().

DriverBase.next() also takes care to store the new sample in the field DriverBase.value, where generate() can employ DriverBase.getValue() to pick this (now previous) sample up for the next sample iteration. DriverBase also offers setValue() and randomizeValue() methods to establish the initial sequence value.

Comments

  1. The present text corrects my Leonardo Music Journal article from 1992, "A Catalog of Sequence Generators". The heading is "1/f Noise", p. 63.
  2. Charles Dodge and Thomas Jerse, Computer Music: Synthesis, Composition, and Performance, 2nd edition, Schirmer, 1997, p. 368.

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