Continuous Fracture Transform1

Introduction

The ContinuousFracture transform divides the driver domain into equal-sized intervals and permutes these intervals. For example if the permutation is (1 3 4 2 0) then ContinuousFracture will divide the driver domain into five equal-sized intervals. Driver values ranging from 0.0 to 0.2 will shift to the range from 0.2 to 0.4. Driver values ranging from 0.2 to 04 will shift to the range from 0.6 to 0.8, and so forth.

Since ContinuousFracture maps the driver domain back onto itself, it can serve as a decorator interposing between a driver and a second transform. And unless the application specifically wants values from zero to unity, decoration is exactly what will happen.

ContinuousFracture introduces conspicuous discontinuities into the 'decorated' driver stream, and it is definitely not something to do with a probabilistic driver that is concerned with sample-to-sample proximity. By contrast, the chaotic drivers, Logistic and Baker, already jump around a lot. Their flavor of dependence has to do with the way their attractors recur quasi-periodically. Trouble is, once Logistic gets going with a specific value of lambda, or Baker gets going with a specific value of mu, then its always the same attractors recurring. What ContinuousFracture offers is the ability to substitute new recurrance points in place of these out-of-the-box attractors.

Usage

The following two panels explore scenarios illustrating how ContinuousFracture might be used. Each panel has five rows, and each row graphs a source sequence on the left and its 'decorated' (permuted) counterpart on the right. The source sequences are held fixed; only the 'decoration' changes. Sequences were 75 samples in length, which was the largest number that would still fit before-and-after information into one horizontal row of graphs.


Figure 1 (a): Panel illustrating how ContinuousFracture reshapes the same Logistic sequence through five different permutations. Each row of graphs provides a histogram of the unpermuted sequence (far left), a time-series graph of unpermuted samples (middle left), a line diagram showing how equal-sized regions of the unpermuted domain map to expanded or contracted regions of the permuted domain (center), a time-series graph of permuted samples (middle right), and a histogram of the permuted sequence (far right).

Scenario #1 appears in Figure 1 (a). It draws a source sequence from the Logistic driver with λ=3.6283 and starting value 0.7.

The source-sequence samples which occupy interval #3 (fourth from the bottom, ranging from from 0.3 to 0.4) shifts to interval #0 (ranging from 0.0 to 0.1) in the first permutation, to interval #9 (ranging from 0.9 to 1.0) in the second permutation, to interval #4 (ranging from 0.4 to 0.5) in the third permutation, to interval #7 (ranging from 0.7 to 0.8) in the fourth permutation, to interval #4 (ranging from 0.4 to 0.5) in the fifth permutation. Except for the shifts, the internal content remains the same: its the same pattern of recurrance and the same succession of relative ups and downs.

Things are a little more problematic with the values clustering around 0.9 in the source sequence. If I understand things correctly, values clustering near 0.9 are associated with the same attractor. Yet the recurrance pattern which is observed around 0.9 in the source sequence passes back and forth between two different intervals after permutation (except in the topmost example, where the stacking of intervals 7, 8, & 9 shifts as a bundle to intervals 2, 3, & 4).

To prevent situations like the one just described would require some way of isolating attractors through analysis of sequence values. I'm not convinced its worth the effort.


Figure 1 (b): Panel illustrating how ContinuousFracture reshapes the same Baker sequence through five different permutations. Each row of graphs provides a histogram of the unpermuted sequence (far left), a time-series graph of unpermuted samples (middle left), a line diagram showing how equal-sized regions of the unpermuted domain map to expanded or contracted regions of the permuted domain (center), a time-series graph of permuted samples (middle right), and a histogram of the permuted sequence (far right).

Scenario #2 appears in Figure 1 (b). It draws a source sequence from the Baker driver with μ=0.8 and starting value 0.55. The sequence of permutations used for Scenario #1 is also used for Scenario #2.

Chaotic sequences will in general be compact, especially when the control parameters λ and μ are at low settings. After processing through ContinuousFracture, the resulting sequences cannot help but being more widely across the driver domain than their chaotic sources.

/**
 * The {@link ContinuousFracture} class transforms a driver sequence by
 * dividing the driver domain into regions which are then permuted.
 * It is a rough and ready way of deriving quasi-periodic sequences from
 * (unflattened) {@link net.charlesames.seqgen.driver.Baker} or
 * {@link net.charlesames.seqgen.driver.Logistic} output.
 * This transform works best with drivers when the incoming driver values
 * have strong attractors; that is, when most driver values are
 * concentrated around sharp peaks in the distribution. It is least effective
 * for relatively uniform driver sequences such as those produced by
 * {@link net.charlesames.seqgen.driver.Lehmer},
 * {@link net.charlesames.seqgen.driver.Balance}, or
 * {@link net.charlesames.seqgen.driver.Brownian}.
 * @author Charles Ames
 */
public class ContinuousFracture
extends TransformBase<Double> implements Transform.Continuous, Driver {
   private Permutation permutation;
   /**
    * Constructor for {@link ContinuousFracture} instances.
    * @param container An entity which contains this transform.
    */
   public ContinuousFracture(WriteableEntity container) {
      super(container);
      permutation = new Permutation(this);
      permutation.setIDQuality(AttributeQuality.UNUSED);
      permutation.setNameQuality(AttributeQuality.UNUSED);
   }
   /**
    * Setter for {@link #itemCount}.
    * @param itemCount The intended {@link #itemCount} value.
    */
   public void setItemCount(int itemCount) {
      checkItemCount(itemCount);
      permutation.fillAscending(itemCount);
      makeDirty();
   }
   /**
    * Check if the indicated value is suitable for {@link #itemCount}.
    * @param itemCount The indicated value.
    */
   public void checkItemCount(int itemCount) {
      if (0 >= itemCount)
         throw new IllegalArgumentException("Item count not positive");
   }
   /**
    * Populate the member array of the embedded {@link Permutation} instance with
    * values derived from a source {@link Permutation}.
    * This method leverages {@link Permutation#iterateMembers(int, int, int, int)},
    * whose documentation further explains the start, increment, multiplier, and
    * transposition arguments.
    * @param source The source {@link Permutation} instance.
    * @param start Starting position.
    * @param increment Position increment between iterations.
    * @param multiplier Multiplier applied modulo N to each member.
    * @param transposition Offset added modulo N to each member.
    */
   public void setPermutation(Permutation source, int start, int increment,
         int multiplier, int transposition) {
      permutation.copyMembers(source, start, increment, multiplier, transposition);
      makeDirty();
   }
   /**
    * Populate the member array of the embedded {@link Permutation} instance with
    * values from a source {@link Permutation}.
    * @param source The source {@link Permutation} instance.
    */
   public void setPermutation(Permutation source) {
      permutation.copyMembers(source, 0, 1, 1, 0);
      makeDirty();
   }
   /**
    * Copy the members of the embedded {@link Permutation} instance to
    * a newly allocated instance.
    * @return A {@link Permutation} instance with the same content as
    * the embedded {@link Permutation} instance.
    */
   public Permutation copyPermutation() {
      Permutation result = new Permutation(null);
      result.copyMembers(permutation, 0, 1, 1, 0);
      return result;
   }
   /**
    * Populate the member array of the embedded {@link Permutation} instance with
    * an ascending sequence of integers.
    * @param itemCount The permutation length.
    */
   public void fillAscending(int itemCount) {
      permutation.fillAscending(itemCount);
   }
   /**
    * Getter for {@link #itemCount}.
    * @return The assigned {@link #itemCount} value.
    */
   public int itemCount() {
      return permutation.itemCount();
   }
   /**
    * Shuffle the region mapping.
    * Do this rarely.
    */
   public void shuffle() {
      permutation.shuffleMembers();
   }
   @Override
   public Double minGraphValue(double tail) {
      return 0.;
   }
   @Override
   public Double maxGraphValue(double tail) {
      return 1.;
   }
   @Override
   public Double minRange() {
      return 0.;
   }
   @Override
   public Double maxRange() {
      return 1.;
   }
   @Override
   protected ContinuousDistribution createDistribution() {
      throw new UnsupportedOperationException("Method not implemented");
   }
   @Override
   protected void validate(DistributionBase<Double> distribution) {
      throw new UnsupportedOperationException("Method not implemented");
   }
   @Override
   public Double convert(double driver) {
      Driver.checkDriverValue(driver);
      double chooser = itemCount() * driver;
      int index = (int) chooser;
      double residue = chooser - index;
      int newIndex = permutation.memberAt(index);
      double value = (newIndex + residue) / (double) itemCount();
      return value;
   }
   @Override
   public boolean hasReset() {
      return getDriver().hasReset();
   }
   @Override
   public void reset() {
      getDriver().reset();
   }
   @Override
   public double getValue() {
      return getDriver().getValue();
   }
   @Override
   public void setValue(double value) {
      getDriver().setValue(value);
   }
   @Override
   public void randomizeValue() {
      getDriver().randomizeValue();
   }
}
Listing 1: The ContinuousFracture implementation class.

Coding

The type hierarchy for ContinuousFracture is:

The ContinuousFracture transform satisfies the Transform interface, in that it draws input from the driver domain; however it turns right around and maps its output right back into the same domain. As such, the ContinuousFracture transform also satisfies the Driver interface. So for example, one could pipe output from Logistic into ContinuousFracture, the pipe the ouput from ContinuousFracture in turn into DiscreteWeighted, whose output could then serve as indices to select, say, a pitch from a scale.

Unlike other classes implementing Transform.Continuous, ContinuousFracture does not subclass ContinuousDistributionTransform. There is no ContinuousDistribution instance, and no quantile function maps raw driver input values to 'decorated' output.

Rather, ContinuousFracture.permutation holds an embedded Permutation instance. The all-important convert() method uses this permutation to remap driver input. The length permutation.itemCount()) determines the number of driver-domain intervals, while the kth permutation element remaps interval k.

Constructing a ContinuousFracture instance extends past allocating the instance with the new operator to determining what permutation the instance will use. There are two approaches:

Comments

  1. The present text is adapted from my Leonardo Music Journal article from 1993, "How To Fracture a Driver Sequence".

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