Minimum and maximum random numbers

I want to generate two random numbers. The numbers will be used to limit a range of hues (minimum 0, maximum 360).

I can do it like this:

float start;
float end;

//…

start = random(0,360);
end = start + random(45,90);
check = start+end;

//then I check whether both start and end are within the 360 range like this:

   if ((check >=0) && (check <=360)) {

      println(start+" "+end+" "+(end-start)); // use the values or throw away

   }

But this seems grossly inelegant and wasteful. Is there a nicer way to find the random start and end values?

1 Like

Perhaps constrain?

2 Likes
float start, end;

start = random(0, 359);
end  =  random(start, 360);
println("start ",start+" end "+end+" range "+(end-start) );

unless you need a minimum range…

That is much nicer than my very slow approach, thank you.

But yes, I do need a minimum and a maximum range. Using your code the range for low ‘start’ values would tend to be far greater than the range for higher values.

I tried for a minimum range (between start and end) of 45 and a maximum range of 90. My code is too slow because nothing good is happening whilst it produces and sifts out the useless results.

@kll

1 Like
float rangemin=45, rangemax=90;
float start, end;

start = random(0, 360-rangemin);
end  =  constrain(random(start+rangemin, start+rangemax), 0, 360); // thanks @Tiemen
println("start "+start+" end "+end+" range "+(end-start) );

3 Likes

That works perfectly. Thanks @kll and @Tiemen
And so neat.

1 Like

It seems like you want to select a random range that is between 45 and 90 wide and falls within a hue. Do you want ranges that can be centered on red? Hue is circular, so ranges from say 315-45 should be possible. Imagine yourself first picking a curved stick of random length 45-90, then placing it somewhere along the edge of a color wheel. Your current approach is to then chop off the right-hand side of the stick if it crosses over the line of absolute red (0/360). This means you have an unusually small number of ranges including the value 0/360, and an unusually large number of those have red as the last color-almost none have red as the first color. Do you want the possible results to be equally distributed?

This method gives you a normal distribution on 0-360 – so, almost no pure red, ever, but it is balanced.

float half = random(45, 90) / 2.0;  // half the range width
float center = random(0 + half, 360 - half);
float start = center - half;
float end = center + half;

Or you could just pick a random range.

float start = random(0, 360);
float end = (start + random(45, 90))%360;

This can give values like “[340, 20]” … but you might actually want that. If you intend to interpolate, you could also just skip the modulo and save “340, 380” as the range – interpolate, then use modulo before passing to a color function.

int[] range = new int[]{200, 400};
colorMode(HSB);
background(255);
for (int i=0; i<width; i++) {
  float c = lerp(range[0], range[1], i/(float)width);
  stroke(c%360, 255, 255);
  line(i, 0, i, height);
}

Another way to think about the constrain approach is that you have created a normal distribution from 0-405 (360+45) and then cropped it on the right side.

This shows the alternative to just make the result equally distributed – choose the size of the range first, then choose a valid random placement for your range based on its size. Now there are no hue ranges that cross red from either side.

/**
 * DistributeRandomRanges
 * 2020-01-06 Processing 3.4
 * https://discourse.processing.org/t/minimum-and-maximum-random-numbers/16888/4
 */
int[] hits;
int[] hits2;

void setup() {
  size(360, 360);
  hits = new int[360];
  hits2 = new int[360];
}

void draw() {
  background(255);
  PVector r = randomRange();
  for (int i=int(r.x); i<int(r.y); i++) {
    hits[i] = hits[i]+1;
  }
  PVector r2 = randomRange2();
  for (int i=int(r2.x); i<int(r2.y); i++) {
    hits2[i] = hits2[i]+1;
  }
  
  // draw current ranges
  stroke(255, 0, 0, 128);
  line(r.x, 20, r.y, 20);
  rect(width-45, 0, 45, height);  // problem on this end, see left edge
  stroke(0, 0, 255, 128);
  line(r2.x, 30, r2.y, 30);
    
  // draw cumulative graph of results
  for (int i=0; i<width; i++) {
    stroke(255, 0, 0, 128);
    line(i, height, i, height-hits[i]);
    stroke(0, 0, 255, 128);
    line(i, height, i, height-hits2[i]);
  }
}

PVector randomRange() { // @kll
  float rangemin=45, rangemax=90;
  float start, end;

  start = random(0, 360-rangemin);
  end  =  constrain(random(start+rangemin, start+rangemax), 0, 360); // thanks @Tiemen
  return new PVector(start, end);
}

PVector randomRange2() { // @jeremydouglass
  // the right hand side is correctly distributed
  float half = random(45, 90) / 2.0;
  float center = random(0 + half, 360 - half);
  float start = center - half;
  float end = center + half;
  return new PVector(start, end);
}

DistributeRandomRanges--screenshot

3 Likes

Wow, this is fantastic and very helpful. Thank you.
Some comments. I am using the LCH color space, but everything you say still holds. For a given L and C, Hue of 90° (for example) is the same colour as -270° and 450°.
Do I want the possible results to be equally distributed? Yes!
Do I want ranges that can be centred on red? I assume this is a rhetorical question. I want the possible ranges to cover the full 0…360. Which they do with your method.

I tried this

PVector randomRangeJ() { // @jeremydouglass
  float start = random(0, 360);
  float end = (start + random(45, 90))%360;
  return new PVector(start, end);
}

It seems that this method is also equally distributed, but is simpler code. Which of your two methods is preferable?

Yes, this gives you random ranges that may also centered on red, which you said what you want. However, in some circumstances this may require a lot of extra handling after you return those values – for example, to lerp() to create a gradient, you can’t just move from one to the other, as this would proceed backwards across the wrong half of the range. For example, if you return [350, 30] and then lerp, you would want the halfway color = 10. But instead you get 190. It depends a lot on what your code that is checking these ranges wants to do. One way to handle it is to return an unwrapped start and end and not %360 until the very end – lerp(350, 390) and then %360 the result, giving you wraparound values. Another is to return split ranges and check the range length. So, if you will be drawing some colors or pixels, return either [180, 220] or return [350, 360, 0, 30]. Check the length of the return, and handle it as two ranges if it wrapped around.

Here is an example randomRange() function that captures all the options discussed so far. Pass it a “mode” String and it will return a random range that fits inside the space, or, if it overflows, is then either cropped, wrapped, split, or simply allowed to overflow. I made it for int

/**
 * RandomRanges
 * useful approaches for generating distributions of random ranges
 * in linear or circular spaces, e.g. hue colorspaces.
 * 2020-01-07 Jeremy Douglass Processing 3.4
 * see https://discourse.processing.org/t/minimum-and-maximum-random-numbers/16888/4
 */

void setup() {
  for (int i=0; i<100; i++) {
    println(randomRange(0, 360, 45, 90, "split"));
    println();
  }
}

float[] randomRange(float minVal, float maxVal, float minWide, float maxWide, String overflowMode) {
  if (overflowMode.equals("fit")) {
    float half = (random(minWide, maxWide) / 2.0);
    float center = random(minVal + half, maxVal - half);
    return new float[]{center - half, center + half};
  }
  float start = random(minVal, maxVal);
  float end = start + random(minWide, maxWide);
  if (end<=maxVal) return new float[]{start, end};
  if (overflowMode.equals("crop")) return new float[] {start, maxVal};
  if (overflowMode.equals("overflow")) return new float[] {start, end};  // redundant, added for clarity
  if (overflowMode.equals("wrap")) return new float[] {start, end%maxVal};
  if (overflowMode.equals("split")) return new float[]{minVal, end%maxVal, start, maxVal};
  return null;
}
3 Likes

This is super useful in itself but also the making a graph of cumulative results and passing strings to test different approaches are transferable to many other projects.

I found the ‘fit’ method worked best for my current project. I was searching for the best ranges to use in Perlin noise, knowing (or guessing) that the answer would vary according to hue. With your help I now have a tool that works. I also found that using modulo or other ways to tidy the results is much better than tidying up afterwards, but that was for this project.

I can’t stand the suspense. :smiley:

Belatedly – hah! that was a cut-paste typo.

I made it for integration into the previous demo sketch. Again, all that complexity isn’t usually necessary – but different forms for different needs.

Here is something that tests each distribution:

/**
 * RandomRanges2
 * 2020-01-09 Jeremy Douglass Processing 3.4
 * useful approaches for generating distributions of random ranges
 * in linear or circular spaces, e.g. hue colorspaces.
 *
 * In this example wrap and overflow are converted inline into the wrap
 * and split format in order to avoid ArrayIndexOutOfBoundsException.
 * In actual code you need only return the needed format.
 *
 * -  split (and wrap, when split) trend towards flat distributions.
 * -  crop (and overflow, when cropped) fall off on the left-hand side.
 * -  fitCenter and fitwidth both have curved distributions.
 * -  fitwidth returns shorter ranges on average, as it ignores minWide
 *    when center is near edge.
 * 
 * see https://discourse.processing.org/t/minimum-and-maximum-random-numbers/16888/4
 */

String[] modes;
float[][] ranges;
int[][] graphs;
int wmax = 360;
boolean reset;

void settings() {
  size(wmax, 480);
}
void setup() {
  colorMode(HSB);
  strokeWeight(3);
  // list different ways to generate a random range
  modes = new String[]{"split", "wrap", "overflow", "crop", "fitWidth", "fitCenter"};
  // each frame store a new range for each mode,
  // and add the cumulative ranges to a graph for each mode
  ranges = new float[modes.length][];
  graphs = new int[modes.length][width];
  // when this flag is set the sketch will screenshot then rerun setup
  reset = false;
}
void draw() {
  background(0);

  // loop over modes
  for (int m=0; m<modes.length; m++) {
    // get range for this mode
    ranges[m] = randomRange(0, 360, 0, 360, modes[m]);  // ! changing 4th argument to more than 2nd breaks fit modes

    // fix bad return ranges by reformatting
    if (modes[m].equals("overflow") && ranges[m][1]>wmax) ranges[m] = new float[]{ranges[m][0], min(ranges[m][1], wmax)};  // crop the overflow
    if (modes[m].equals("wrap") && ranges[m][1]<ranges[m][0]) ranges[m] = new float[]{0, ranges[m][1], ranges[m][0], wmax-1};  // split the wrap

    // add range(s) to graph
    if(ranges[m].length>6) print(ranges[m].length, " ");
    for (int r=0; r<ranges[m].length; r+=2) {
      for (int x=(int)ranges[m][r]; x<(int)ranges[m][r+1]; x++) {
        graphs[m][x]++;
        // if a graph height has filled the page, reset sketch at end of this frame
        if (graphs[m][x] > height-50) reset = true;
      }
    }
  }

  for (int m=0; m<modes.length; m++) {
    // draw range bars and graphs
    int h = m * (360-120)/modes.length;
    stroke(h, 255, 192);  // set mode line color
    int lb = (m+1)*5;
    line(ranges[m][0], lb, ranges[m][1], lb);
    if (ranges[m].length==4) line(ranges[m][2], lb, ranges[m][3], lb);
    for (int x=0; x<width; x++) {
      point(x, height-graphs[m][x]);
    }
  }
  for (int m=0; m<modes.length; m++) {
    // draw labels
    int h = m * (360-120)/modes.length;
    int tab = m * (wmax-20)/(modes.length);
    fill(0);
    textSize(16);
    text(modes[m], tab, height-graphs[m][tab]);
    fill(h, 128, 255);
    text(modes[m], tab+1, height-graphs[m][tab]+1);
  }

  // if resetting, save frame first;
  if (reset) {
    saveFrame("RandomRanges2--screenshot#####.png");
    frameCount = -1;  // rerun setup;
  }
}

float[] randomRange(float minVal, float maxVal, float minWide, float maxWide, String mode) {
  //if (maxWide > maxVal-minVal) throw new IllegalArgumentException("maxWide cannot be greater than value range.");
  // if the mode is to "fit" the random range inside the min-max values then
  // fitting can happen width-first (fit center) or center-first (fit width).
  switch(mode) {
  case "fitCenter":  // random width, fit center
    float wide = random(minWide, maxWide);
    float center = random(minVal + wide/2.0, maxVal - wide/2.0);
    return new float[]{center - wide/2.0, center + wide/2.0};
  case "fitWidth":  // random center, fit width -- ignores minWide and limits range if center near edge
    float center2 = random(minVal, maxVal);
    float wideLimit = 2 * min(center2, maxVal-center2);
    float wide2 = random(min(wideLimit, minWide), min(wideLimit, maxWide));
    return new float[]{center2 - wide2/2.0, center2 + wide2/2.0};
  }
  // otherwise, just pick a start and end, which might overflow
  float start = random(minVal, maxVal);
  float end = start + random(minWide, maxWide);
  // did end overflow? if so, choose a format to represent the result
  if (end>maxVal) {
    switch(mode) {
    case "crop": 
      // crop range, throw away invalid part
      return new float[] {start, maxVal};
    case "split": 
      // crop range, add overflow as wrapped (valid) ranges
      int wraps = (int)((end - end%maxVal)/maxVal);
      float[] result = new float[2*wraps + 2];
      for (int i=0; i<result.length; i+=2) {
        result[i] = minVal;
        result[i+1] = maxVal;
      }
      result[0] = start;
      result[result.length-1] = end%maxVal;
      return result;
      //return new float[]{minVal, end%maxVal, start, maxVal};
    case "overflow": 
      // return invalid part of the range anyway
      // using these values to iterate or interpolate may cause errors
      return new float[] {start, end};
    case "wrap": 
      // wrapping around last-first, for example: [350, 370] -> [350, 10]
      // always returns a range with 2 endpoint, e.g. [100, 3800] -> [100, 200];
      // using these values to iterate or interpolate may cause errors
      return new float[] {start, end%maxVal};
    default:
      throw new IllegalArgumentException("Invalid mode on overflow: " + mode);
    }
  }
  // no overflow: range already fits
  return new float[]{start, end};
}

Result:

RandomRanges2--screenshot

Thank you. This is so good, and I am glad I asked.

1 Like