In the driver/transform design, an active Driver
generates an intermediate value u in the
continuous "driver domain" from zero to unity, while a passive Transform
distributes u values over whatever "target range" a
particular application requires. This target range can be either discrete (values are integers) or continuous (values are real — floating point — numbers).
Thus arose the paradigm of a system coupling an active “driver” with a passive “transform”. My usage of “driver” came from acoustics, where a “driver” indicates an active component (e.g. the vocal cords) that would be coupled with a passive resonating system (e.g. the cavities of the mouth and nose). The range from zero to unity became known as the “driver range”.
If one can generate a driver sequence whose values X_{0}, X_{1}, X_{2},
…, X_{N-1} are uniformly distributed in the continuous domain from zero to unity (and the conventional random-number
generator is an example of this), then one can conform this sequence to any desired distribution by applying an appropriate statistical transform. How
to do this will be explained later. Hence we gain modularity and flexibility without losing generality if the
Driver
contract stipulates that
continuous sequences be 'normalized' to range from zero to unity. Readers should bear in mind that non-uniformity in the
driver sequence will persist through the transformation into the final result. The overall target range will be valid, but local concentrations
inside that range may stray from the distribution. Since it is difficult to estimate in advance how nonuniform drivers will affect a transform, it is often
helpful to graph an empirical histogram of driver/transform output so one can evaluate the resulting distribution visually.
Within the Driver/Transform design, the
Transform.convert()
method acts as a mathematical function mapping values from a source set, the
domain, to a target set, which in my day was called the
range.
I have adopted the term driver domain
for the source set, since this set describes values produced specifically by
Random.nextDouble()
and more generally by Driver.next()
.
The driver domain is synonymous with the probability domain associated with statistical distributions,
though the contexts differ.
Mathematically, driver domain values are real numbers. Real
numbers are "continuous" in the sense that given any two numbers which are different (but possibly very close), the number half-way
between is also a real number. This means that real numbers
can be represented — up to a certain precision — in binary form with digits to the right of the binary point.
Real numbers are thus distinguished from integers which are discrete and which have no digits
right of the binary point. Real numbers are implemented on this site using Java's double
type;
other representations are available (e.g. float
), but double
is
what most hardware floating-point processing units work with.
The driver domain runs from zero to unity. But for unity, driver values all have zero to the left of the binary point.
Experience gained from Random.nextDouble()
shows this:
Having the driver domain extend continuously from zero to unity greatly simplifies the calculations needed to convert to other ranges. For example
mapping the driver value u
into the continuous range from A
to B
uses the calculation:
(B-A)*u + A
.
Likewise, mapping the driver value u
into the discrete range from 0
(inclusive) to N
(exclusive) uses the calculation:
Math.floor(N*u)
.
Figure 1 illustrates why uniform driver values are desirable when the distribution matters. Two graphs are presented. Each x-axis plots the driver range from zero to unity. Each y-axis plots the discrete target range {0, 1, 2}. The transformation from driver to target output is accomplished by step functions, which in each graph map 1/3 of the driver range to outcome 0, 1/3 of the driver range to outcome 1, and 1/3 of the driver range to outcome 2. The vertical green lines represent specific driver values, while the rows of green dots tally how many verticals intersect with the step function at that particular output.
A statistical transform is reliable only to the extent that its driving input is uniform. Yet among the implementators of
Driver.next()
:
Balance
distributes output values uniformly over
the short term.
Lehmer
(the standard random number generator) tends
toward uniformity as the population size grows large enough for the Law of Large Numbers to exert influence.
DriverSequence
will produce uniform sequences under
two conditions: (1) the population of source samples is itself uniformly distributed, and (2) the sequence length is an integer
multiple of the source population size.
ContinuousLevel
transform (right).
Most other implementations of Driver
do not
strive in any way for uniformity. For example, the sequence on the left side of Figure 2 was generated
using the Brownian
driver. None of these sequence values fall in
the range from 0.0 to 0.3. The most dense region is that between 0.6 and 0.7; that's where the sequence lingered most during this
particular run.
Referring back to Figure 1, if you are employing a transform for the purpose of adapting driver-domain values to the needs of a particular application, it may not be of great concern to you that range values 0 and 2 are over-represented by comparison to range value 1. However if conformance to the distribution is essential to what you are trying to achieve, then you need some way of performing the operation shown in Figure 2, which retains the up-and-down contour of the original Brownian sequence, but which compresses sparsely populated regions (reducing empty regions down to nothing) and expands densely population regions so that equal-sized intervals afterward contain roughly the same number of samples.
The ContinuousLevel
unit performs the operation illustrated in
Figure 2. Although built upon the Transform
base class,
ContinuousLevel
also implements the Driver
interface. Thus ContinuousLevel
maps driver-domain values
right back into the driver domain, adapting non-uniform output from a 'real' Driver
into uniform input for another Transform
.
WRAP
ContainmentMode
.REFLECT
ContainmentMode
.
The majority of processes implementing the Driver
operate between well-defined lower and
upper bounds which are thus readily scalable to the driver domain from zero to unity.
In principle one should be able to exploit any desired sequence, regardless of origin. The capability to
accept fully fleshed-out value sequences and to rescale these to the driver domain is particular to the
One exception is DriverSequence
, which presents
values sequentially from a stored array which must be populated prior to the first call to
DriverSequence.next()
. However since the entire
source set is known in advance, it is straightforward enough to ascertain lower and upper bounds, then use these bounds
to rescale the set.
Two other exceptions are Brownian
and
Bolt
, both of which are based upon
Brownian motion. Brownian motion starts with some arbitrary location, and
with each iteration moves randomly from wherever it was to wherever it will be.^{1}
The identical gray contours in Figure 3 and Figure 4 exemplify Brownian
motion, plotting locations along the y-axis and time (actually sequential order) along the
x-axis.
Driver.next{}
accomodates either the left-right (x) or the up-down (y) component, but
a pair of generators will produce two-dimentional motion if desired.
© Charles Ames | Page created: 2022-08-29 | Last updated: 2022-08-29 |