Motivation

I was not successful in producing good results with the Voss- McCartney algorithm as described by Allan Herriman and listed in the "DSP generation of Pink (1/f) Noise" web article at http://www.firstpr.com.au/dsp/pink-noise/.

Here was the problem. As indicated by the text-art illustrations on that site, each component stage in the Voss-McCartney generator produces a sequence of randomly modulated squared pulses. For pulses produced at a given frequency, they produce spectral energy distributed according to the familiar/accursed sin(x)/x form. That is, the spectral energy distribution is NOT SMOOTH. Certain frequencies are supplied plenty of energy, and certain others get zero. The following plot shows the power spectrum of a single simulated stage. Please note, this is not a plot of theoretical formulas, it is from numerical operation of the generator and lots of averaging.

One stage of Voss-McCartney generator

When multiple generator stages of the Voss-McCartney generator are combined, each one contributes energy in the same non-smooth manner, with each one compressed more toward the low-frequency end of the spectrum, making it less able to compensate for anomalies produced by the higher frequency updates. The combination depends on the harmonic frequencies, and it is not random. If the peaks of one power distribution happen to line up well with the valleys of previous ones to compensate, the combination could well approximate the 1/f power characteristic. But otherwise...

I experimented with this empirically, simulating the generator and using FFT methods to analyze 10000 replications of 1024-term sample blocks. The power distribution characteristic was starting to show the desired 1/f form, but it was clear that a very large number of stages would be needed to have any chance of an accurate approximation. For example with eight stages, compare actual results to the ideal 1/f model:

Eight-stage Voss-McCartney generator spectrum

It was pretty clear that this was not converging uniformly and rapidly to an accurate fit.

Why This Result?

It seemed to me that the reason for the non-smooth spectrum curves was the fact that the updates are highly predictable. If a low frequency generator is updated, there are only certain discrete times at which this will happen. It is this high degree of non-randomness, so I conjectured, that causes the repeatably irregular spectrum shape.

My idea was this: would the generator performance be better if the generators were updated at the same rates ON THE AVERAGE, but not on a rigid schedule? That is, the times at which updates occur are randomized to occur with probability p for the first stage, probability p/2 for the second, ... probability p/2N for stage N. With this scheme, each generator is updated the same as the Voss- McCartney generators on the average.

In the following experiment, I picked a single stage and updated it repeatedly. I then did an FFT analysis, scaled the result, and obtained the following comparison to the ideal 1/f spectrum (I believe this is a magnitude rather than power spectrum)

Experiment with one randomized generator

This was an encouraging degree of similarity.

Expanding the Randomized Generator Experiment

I tried adding more stages, but I was disappointed with the results. The range was extended downward as expected, but the tracking of the 1/f shape was not accurate.

Unsatisfactory fit with original uniform weighting

I suspected that this was related to the aliasing phenomenon in sampled data systems, with higher frequencies reflecting in the sampled data set as distortions of the desired curves leading to a flattening of the spectrum. Wishing to avoid an exact analysis at this point, I wondered whether a different scaling on the generator terms would produce a more accurate fit. Here is the Stochastic Voss-McCartney Algorithm after generalizing the stage weighting parameters. The urand function is a uniform pseudo-random generator on balanced interval -1 to 1, obtainable from generators on interval 0 to 1 by the mapping 2*(rand-0.5).

  Weighted Stochastic Voss-McCartney Algorithm (first version)

  For each update:
    level  := urand[0,1]

    output := urand[-1,1]*W1
    if (level>0.5) break;
      output := output + urand[-1,1]*W2
    if (level>0.25) break;
      output := output + urand[-1,1]*W3
    if (level>0.125) break;
      output := output + urand[-1,1]*W4
    if (level>0.0625) break;
      output := output + urand[-1,1]*W5
    if (level>0.03125) break;
      output := output + urand[-1,1]*W6
    if (level>0.015625) break;
      output := output + urand[-1,1]*W7
    if (level>0.0078125) break;
      output := output + urand[-1,1]*W8
    if (level>0.00390625) break;
      output := output + urand[-1,1]*W9
  End

I experimented with another variation that generated the level decision random variable for each test independently and this didn't make any difference. For my first tests, the scaling parameters W1 through W9 were initially set equal, as in the original Voss-McCartney algorithm, and then I began tweaking manually to see where this would lead. I ended up with the weighting factors

Weighting factors: W1 - W9
    .252
    .672
    .000
    .171
    .190
    .286
    .175
    .233
    .021

The attached illustration shows simulated results of the 8-stage random generator with adjusted weights. Don't get too excited about this, because we can do better.

Eight-stage randomized pink noise generator

An Additional Generalization

The spectrum has the shape it does because of correlation between terms in the composite generator. In the analog world, the 1/2, 1/4, 1/8 sequence for probability of updating should produce the right shape, but that is an infinite sequence and it breaks down in sampled data systems. As a second generalization of the original algorithm, I considered allowing adjustment of the correlation by adjusting the probability levels for updating at each stage. This yielded the (final?) Stochastic Voss-McCartney Algorithm.

  Weighted Stochastic Voss-McCartney Algorithm 

  For each update:
    level  := urand[0,1]

    output := urand[-1,1]*W1
    if (level>L1) break;
      output := output + urand[-1,1]*W2
    if (level>L2) break;
      output := output + urand[-1,1]*W3
    if (level>L3) break;
      output := output + urand[-1,1]*W4
    if (level>L4) break;
      output := output + urand[-1,1]*W5
    if (level>L5) break;
      output := output + urand[-1,1]*W6
    if (level>L6) break;
      output := output + urand[-1,1]*W7
    if (level>L7) break;
      output := output + urand[-1,1]*W8
    if (level>L8) break;
      output := output + urand[-1,1]*W9
  End

This adds no new complexity to the code, but it does add another degree of difficulty to the analysis. How do you pick the right correlation parameters L, and how good of a fit is possible? It was time to do the homework. Along the way, I found an alternative scheme for the individual generator stages.

Correlated Generators

Generator Type 1

Consider the following stochastic processes, based on the randomized version of the Voss-McCartney generator stages.

    Rk =
    |  Rk-1 with probability p
    |  r distributed U(-1,1) with probability (1-p)

The random variable r is an uncorrelated random number as approximated by a uniform random number generator algorithm. With each sample the value of generator R either remains the same, with perfect correlation, or it is allowed to change to a new value completely independent of all past history. The less often a generator's value changes, the more its output looks like a low frequency pulsed wave, and the more the generator stage contributes its signal energy at low frequencies.

Properties of the distribution of r:

    E(r) = 0            (zero mean)
    E(r2) =  1/3         (variance)

From these properties of r we can immediately deduce the expected value

    E(Rk·Rk) =  1/3

Considering a one step delay, the autocorrelation at this interval depends on

    Rk · Rk =
    |  R2k-1   with probability p
    |  Rk-1 r   with probability (1-p)

But from independence, the expected value of the second product pair will be zero, so we will obtain

    E( Rk · Rk-1 ) = p/3

Continuing in a similar manner by induction, and observing that the correlation function is symmetric, we obtain in general

    E( Rk · Rk-N ) = p|N|/3

In other words, the correlation sequence is exponential. But there is another way to generate the exponential correlation in a random sequence, which I already knew about, and this leads to another form of generator stage that can be used with equivalent effect.

Generator Type 2

Consider the alternative generator form that is updated at every sample but with a partial replacement, adjusted by a parameter p in the open interval (0,1).

  Rk = Rk-1·p + r·(1-p)

The algebraic derivation is omitted, but it can be demonstrated that

     E( Rk · Rk ) = (1-p) / 3(1+p)

and from there that

     E( Rk · Rk-N ) = (1-p) / 3(1+p)  · p|N|

That is to say, the correlation is once again exponential, and differs only by a scaling factor depending on parameter p, hence the adjustment to make the two generators work equivalently for the pink noise generation is a simple scaling.

The second generator form is just the discrete time equivalent of a linear first-order lag filter. Increasing p and increasing the correlation is closely related to increasing the "lag time constant" in a linear first-order lag, which means that larger values of p result in a lower cutoff frequency.

Possible advantages from this second correlated generator form: the power distribution might show a little less variance over the short run, and the computational load is uniform for every update. I also speculate that higher order statistics would correspond better to a true pink noise process. The amplitude coefficients I provide later do not take into account the additional scaling factor, so you would need to adjust by (1+p)/(1-p) when applying the second generator form.

For completeness, the following is the stochastic correlated generator algorithm using the second type of generator stage.

  Correlated Generator Algorithm 

  state[1:Nstages] = 0.0;
  for each update:
    output = 0;
    for iSt = 1:Nstages
      state[iSt] = P[iSt]*state[iSt] + (1-P[iSt])*urand(-1,1)
      output = output + Wt[iSt]*state[iSt]
    end
  end

Power Spectrum Analysis

After an unbelievable length of time messing around with approaches that were nonproductive, I settled down to basic z- transform theory and found the mathematical model that exactly describes the power spectral density contributed by each individual generator stage. The combined power of the independent sources is then the sum of the spectral power from each generator.

Start with the autocorrelation function for the generator, ignoring the extra scaling factor that will be present for the type-2 correlated generator stage.

    E( Rk · Rk-N ) = p|N|/3

Applying the definition of a z transform and splitting the infinite sum into a positive one-sided transform, negative one-sided transform, and constant term, we get the power spectrum

   S(z) = sum( Rn z-n )   over all n

        = 1/3 · 1/(1-pz-1)  +  1/3 · 1/(1-pz)  -  1/3

An algebraic reduction yields the equivlent formulation

       1/3 · (1-p2) z-1
     ---------------------------
       (1+p2)·z-1 - p·(1+z-2)

For purposes of evaluating the power spectrum, we can ignore the z-term multiplier in the numerator as this is just a time shift, which doesn't matter. Otherwise, we can make the usual substitution

 z = ej·2·pi·n/N 

where N is the number of frequencies to sample between 0 and the sampling rate, and n is the frequency term index. However, due to the special symmetry in this expression, the remaining imaginary terms cancel, and we end up with a simple analytical form.

            1/3 · (1-p2)
   S(n) = -----------------------
           (1+p2) - 2p·cos(An)

where angle step A equals 2·pi/N.

The relevance of this is that we now have a relatively simple and very precise numerical formula that says exactly how much signal energy each generator stage is going to introduce at every frequency, as a function of correlation adjustment parameter p. Thus, we have all of the information necessary to numerically optimize both the generator probability parameters and the generator scaling factors, for any given number of generator stages M.

Optimizing the Generator Parameters

There is no hurry in doing this, as the problem is small scale, and I used a slow, steady, reliable — did I mention slow? — gradient algorithm to tune the coefficient settings. Unfortunately, the dependence on the p parameter is nonlinear, so a nonlinear optimization is necessary.

I used Matlab code to perform this optimization and display the results, and I can make it available to anybody who wishes to pursue this further, with the understanding that it comes with no warranty and you are on your own to correct the errors and make it work right. For most people, the following parameter sets should cover most requirements. These seem to take the stochastic generator algorithm very close to the absolute limits of what it can do.

Number of
terms
Band covered Fit error Coefficients
3 2.5% to 80% of Nyquist frequency
(5 octaves)
0.25 dB
a = [ 0.01089  0.00899  0.01359 ]
p = [ 0.3190   0.7756   0.9613  ]
4 1.25% to 75% of Nyquist frequency
(6 octaves)
0.15 dB
a = [ 0.01000   0.00763  0.00832  0.01036 ]
p = [ 0.3030    0.7417   0.9168   0.9782  ]
5 0.8% to 80% of Nyquist frequency
( 7 octaves )
0.15 dB
a = [ 0.00224  0.00821  0.00798  0.00694  0.00992 ]
p = [ 0.15571  0.30194  0.74115  0.93003  0.98035 ]

Here is a listing of Matlab code extracted from my test scripts. It is configured for a three-stage generator, using the terms given above.

   Executable m-script fragments for 3-stage generator. 

  % ---- These lines are for initialization, executed once ----
  Ngen = 3
  av = [ 4.6306e-003  5.9961e-003  8.3586e-003 ]
  pv = [ 3.1878e-001  7.7686e-001  9.7785e-001  ]

  % Initialize the randomized sources state
  randreg = zeros(1,Ngen);
  for igen=1:Ngen
      randreg(igen)=av(igen)*2*(rand-0.5);
  end
  % ---- End of initialization sequence -----------------------

  % ---- These lines are executed with each evaluation --------
  % Note: 'rand' is U[0,1] and changes with each evaluation.
  rv = rand;

  % Update each generator state per probability schedule
  for igen=1:Ngen
    if  rv > pv(igen)
      randreg(igen) = av(igen)* 2 *(rand-0.5);
    end
  end

  % Signal is the sum of the generators
  sig(ix)=sum(randreg);
  % ---- End of one generator evaluation pass -----------------

Following is a plot of the 5-term generator superimposed on the ideal 1/f curve. Yes, that's right... There isn't enough pixel resolution in this plot for the differences to show up. There are anomalies at the very lowest frequencies however, as you must expect, as a generator with finite terms cannot approximate infinte power at frequency zero (and you don't want it to anyway!) It is easy to prove from the fact that all generators are balance that the power density is zero at frequency 0.

Final plot of best fit

For most purposes, I can recommend the three term generator. What do more terms gain you? Basically, extended low frequency range. But a lot of the time, you will need to highpass limit low frequencies anyway to keep the energy adequately bounded.

It appears that the limit of accuracy for this method is about 0.15 dB, which is a relative error of about 1.5% from low frequencies through about 80% of the Nyquist frequency. The generator curves are not an orthogonal family and there is no reason to suppose that adding more generators will produce a lot of improvement. There is little power left in the remaining high end of the spectrum and it probably doesn't matter too much. For example, at the Nyquist frequency, the signal power density should be reduced to 0.195% but it is actually reduced to 0.234%. The difference of about 0.04% is not likely to damage many applications.

 

 

 

This page and its contents are licensed under a Creative Commons Attribution 4.0 International License. For complete information about the terms of this license, see http://creativecommons.org/licenses/by/4.0/. The license allows copying, usage, and derivative works for individual or commercial purposes under few restrictions.