Sequence generators implementing the Driver
interface provide the active component of the
Driver/Transform design.
To satisfy Driver
, a generator must implement a next()
method returning
Double
values ranging continuously from zero to unity.
Sometimes the passive Transfer
component seeks not just to conform the
output of the Driver/Transform coupling just to a range, but also to a distribution.
Under this circumstance, values from next()
should additionally be distributed uniformly. However
this circumstance does not always apply, so
uniformity is not enforced by the Driver
interface. (Which would not in fact be easy to do.)
The Transform
interface encourages adherence to these purposes by establishing mandatory
next()
and hasNext()
methods. These method
names inherit from the java.lang.Iterator
interface, whose approach to presenting sequences does away with explicit indexing.
The prototype for my Driver
interface is the standard number generator implemented by Java
as java.util.Random
.
Indeed my motivation for this abstraction was to develop a stable of sequence generators where
Driver.next()
could readily be swapped into applications where
Random.nextDouble()
might initially be considered.
The primary criterion for evaluating drivers is dependence. A sequence is said to be dependent when the sequence is in
some sense redundant or predictable. All but one of the Driver
instances described
on this site are dependent in some way or another; the exception is the
Lehmer
driver, which wraps
java.util.Random
. I have taken great pains
elsewhere to work out how to verify numerically that sequences
produced by Random.nextDouble()
are independent,
which result directly concerns Lehmer. In theory this same test should
fail for every other Driver
implementation; we'll see if this actually happens.
The Driver
units which I have implemented divide into three categories: deterministic,
probabalistic, and chaotic.
Of deterministic Driver
units there is just one:
DriverSequence
.
DriverSequence
presents successive values from a stored array.
If the values in the stored array are uniform, then DriverSequence.next()
produces ideally uniform sequences over frames with that number of samples. There are various ways to go about this. The most direct is to
fill the array with values that are equally spaced between zero and unity, then somehow permute the
stored values into non-ascending order.
The flavor of dependence exhibited by DriverSequence
is exclusion: the fact that something happened
recently supports the prediction that the same thing will not happen again soon.
The DriverSequence.hasNext()
methods returns true
until
the stored array is exhausted, then false
. A call to DriverSequence.reset()
is required to restart the cycle.
Of probabalistic Driver
units there are Lehmer
,
Balance
,
Moderate
, Borel
,
Brownian
, Bolt
,
Rosy
, and Voss
.
Lehmer
wraps Java's standard random number generator to produce values
which are probablistically independent.
Balance
produces sequences which are balanced in
this sense: all subsets (even overlapping ones) are as uniformly distributed between zero and unity as that many samples can be. This is another flavor of
exclusion dependence; Balance
actively strives to locate samples in underrepresented regions, which is the converse
of avoiding overrepresented regions.
Moderate
, Borel
,
Brownian
, and Rosy
,
and Voss
, each implement
flavors of proximity dependence.
All probabilistic drivers with the exception of Bolt
have their
hasNext()
methods hard-coded to return true
.
Each call to Bolt.reset()
fills a stored array with a Brownian sequence from an indicated origin to an indicated goal (both driver-domain values) over an indicated sequence length.
The Bolt.hasNext()
methods returns true
until
the stored array is exhausted, then false
.
From most probabilistic drivers there is a tendency for consecutive samples to follow close upon one another. How closely they follow is something that
can be measured numerically, and this measure
of proximity is one concrete way of distinguishing Driver
behaviors.
Given a sequence X0, X1, X2, …, XN, then we want to measure how close the Xjs are to their Xj-1s. This quantity can be calculated using one of two methods.
The absolute value method computes a simple average. It calculates the difference between each pair of consecutive samples, takes the absolute value of each difference, sums up the absolute values, then divides this sum by N-1 (the number of pairs):
|
ΣN-1 j=0 | |Xj+1 - Xj| |
The root-mean-square (RMS) method computes a statistical deviation. It squares the difference between each pair of consecutive samples, sums up the squares, divides this sum by N-1, then takes the square root of the result:
√ |
|
Opinions differ upon which method is preferable under which circumstances, but it seems to come down to the issue of outliers. Outliers are individual values which stray more than normal from the calculated norm. The RMS calculation is more sensitive to outliers. Since the present site celebrates outliers, it favors the RMS calculation over the absolute value calculation.
Driver Class | 100 Samples | 1000 Samples | 10000 Samples | 100000 Samples |
---|---|---|---|---|
Lehmer | 0.435 | 0.433 | 0.410 | 0.409 |
Balance | 0.485 | 0.483 | 0.482 | 0.483 |
Moderate | 0.254 | 0.234 | 0.234 | 0.235 |
Borel | 0.343 | 0.348 | 0.353 | 0.353 |
Table 3 lists sample-to-sample proximities for the Lehmer
,
Balance
, Moderate
, and
Borel
drivers.
The Brownian
driver has a
deviation
parameter which directly controls the
average distance between consecutive samples. When the deviation
is smaller than 0.25
and the containmentMode
is set to
REFLECT
, then the
deviation
correlates very closely to calculated RMS proximity measure.
Similar considerations apply to the Bolt
driver.
The Voss
driver has a
depth
parameter which directly controls the
number of sample-and-hold streams. As the number of streams increases, values cluster in an ever-tightening
bell curve focused half-way beween zero and unity.
The Rosy
driver have
retention
and rolloff
parameters
which controls how long the most significant bits hold on to their set/clear states before reselecting. When
retention
is near its lower limit of zero, the Rosy
driver produces standard randomness. As retention
increases toward its upper limit of unity,
Of chaotic Driver
units there are Logistic
and Baker
. These units implement chaotic algorithms to produce
patterned sequences of variable complexity. Chaotic sequences generated by
Logistic
and Baker
are decidely nonuniform. They tend rather to points of extreme concentration (so-called
attractors), separated by rarefied gaps.
The flavor of dependence exhibited by DriverSequence
is patterning.
Both chaotic drivers have their
hasNext()
methods hard-coded to return true
.
Driver
interface.
MathMethods
static helper-method class.
Up until very recently I had thought that Java interfaces specified methods shared between classes implementing the interface, but nothing else, certainly not code. Recently I discovered that the Eclipse IDE accepts static variables, static methods, and also enumerations. This makes it possible not just to say what methods are available, but to provide code support for range checks and other conformance issues.
The Driver
interface presented as Listing 1 leverages
java.lang.Iterator
to define the all-important next()
method (also hasNext()
). It
also provides boolean
valueInRange()
and exception-throwing checkDriverValue()
methods to enforce holding driver values to the
range from zero to unity. The ContainmentMode
enumeration provides options for bringing sequences
back into the driver domain when they threaten to wander out. Finally, the interface provides access methods to a value
property.
Package consumers can prescribe the first sequence element by calling setValue(double)
prior to the first call to next()
. The value
property
can also be randomized by calling randomizeValue()
. Access to the current value (which changes with each
call to next()
) is provided by the getValue()
method.
Let u
be a driver value, that is, a double
between zero and unity.
Then the value
property imposes this contract upon the implementing classes: immediately after invoking
setValue(u)
, a call to next()
will return u
. Expressed another way, let
be the sequence of values generated by next()
. Then Xk will be
the value returned by getValue()
prior to the kth call to
next()
. The kth call to
next()
will also return Xk.
DriverBase
base class.
The abstract DriverBase
class presented as Listing 3 encodes shared functionality
for most units implementing the Driver
interface.
The setValue()
, getValue()
, and
randomizeValue()
methods prescribed by the Driver
interface
are, here in class DriverBase
, directed to a value
instance variable.
The contract which requires that whatever the package consumer puts into setValue()
comes back immediately from next()
, is enforced within next()
by
delegating sample generation to a generate()
method, which specific subclasses must implement.
Notice that generate()
is not present in the public-facing
Driver
interface, but rather held protected
by DriverBase
so that only DriverBase
and
its subclasses are aware of the method.
Three actions are undertaken by next()
:
value
instance variable's previous content is held aside in the local variable
oldValue
.
value
instance variable
is overwritten by a new result from generate()
.
oldValue
local variable (not value
)
that is returned by next()
to the package consumer.
Notice that next()
is designated final
. This prevents
the implementing class from supplying a next()
override which countermands the contracted
behavior.
© Charles Ames | Page created: 2022-08-29 | Last updated: 2022-08-30 |