Java classes implementing the Transform
interface provide the passive component of the
Driver/Transform design.
Transform
instances serve two purposes. Their primary purpose is adaptive: converting
from the driver domain (zero to unity) to whatever range suits the demands of a specific application.
The secondary purpose is distributive: influencing how values concentrate within various regions of the application range.
The Transform
interface encourages adherence to these purposes by establishing a mandatory
convert()
method. This method does the actual work of transforming
driver-domain values into application-range values.
While it is likely that these driver-domain values will be provided by
Driver.next()
,
this is not a requirement. However calling convert()
with an argument outside
zero to unity will throw an IllegalArgumentException
.
Alternatively, there are next()
and hasNext()
methods
which tighten the transform's coupling to its driver. These method names emulate Java's
Iterator
interface, whose approach to presenting sequences does away with explicit indexing. It also exemplifies the
decorator design pattern where the Transform
relays data requests to a Driver instance, which latter plays the role of the "decorated object".
The next()
method employs convert()
to do the "decorating".
Concerning the secondary, distributive purpose of Transform
instances, there is
something everyone needs to understand: Although a lot of trouble is taken to influence where in the range output values concentrate and
where they disperse, these efforts are effective only to the extent that the driver values are uniform. Uniformity means that
if you divide the driver domain into some number of equal-sized intervals, then each interval
should contain approximately the same number of driver values. The standard random number generator is uniform over the long
term, but I have demonstrated graphically elsewhere that sequence lengths as long as 32 are hardly
uniform. The balanced-bit generator produces short term uniformity, but the resulting
sequences jump around a lot. If the sequence length
N is known in advance, a good way to obtain uniformity is to generate
N values equally spaced between zero and unity, then somehow order the items — perhaps by
random shuffling or perhaps by deterministic serial
techniques. Pretty much every other Driver
implementation described on this site produce sequences which are less uniform than standard-random sequences, balanced-bit sequences,
or equal-increment sequences.
Application ranges can be discrete or continuous. They can be bounded or unbounded. A range, combined with information concerning how
values concentrate within the range, makes a statistical distribution. I am here deliberately using the term "statistical
distribution" rather than probability distribution even
though Wikipedia redirects the former term to the latter. My point in drawing this distinction is that in the context of distributions,
the term "probability" specifically indicates a population density expressed as a
proportion of the total population. Chance doesn't enter into the distribution concept. Remember that the
Transform
is the passive component of a
Driver/Transform coupling. If chance is involved, the involvement
must happen within the active Driver
component.
The mathematical theory of distributions is well developed. In particular, knowing the
probability density function (PDF)
that describes how thickly values concentrate in each portion of the range, one can derive a
quantile function which maps uniform driver values to application-range
values, with the indicated concentrations. That is exactly what is needed to implement a
Transform
. And I have two distribution classes which take
a numerically expressed PDF and give back quantile values:
DiscreteDistribution
when the application range consists of integer values, and
ContinuousDistribution
when the application range consists of real numbers.
So it should not be surprising that most (but not all) of the Transform
classes described on this site leverage one or the other of these two Distribution
classes.
Transform
interface.
Much of the functionality for my concrete Transform
classes is implemented in base classes. The type
hierarchy for Transform
classes starts off with TransformBase
.
Immediate subclasses include DiscreteDistributionTransform
and ContinuousDistributionTransform
.
DiscreteDistributionTransform
embeds a
DiscreteDistribution
instance to manage value-weight mappings
for discrete transforms.
ContinuousDistributionTransform
embeds a
ContinuousDistribution
instance to manage trapezoidal approximations
to continuous probability curves.
The
base classes take care of methods like convert()
, hasNext()
,
and next()
. Concrete Transform
classes
correspond to specific distribution formulas, and those formulas have specific parameters which need to be named, stored, and range-checked
on a case-by-case basis. The concrete classes also have the job of monitoring parameter changes and of requesting recalculations in that event.
The Transform
interface presented as Listing 1
enforces commonality of method calls.
Notice in Listing 1 that the Transform
interface has a generic parameter T
which
extends java.lang.Number
.
Number
is an abstract base class whose implementing sub-classes include
Integer
and
Double
.
In practice, T
can be either of these.
Transform.Discrete
(near bottom of Listing 1) particularizes
Transform
by setting T
to Integer
.
Transform.Continuous
(bottom of Listing 1) particularizes
Transform
by setting T
to Double
.
Package consumers should reference the particularized interfaces in favor of the generic one. If the interface conventions
had allowed me to make Transform
protected
while making Transform.Discrete
and Transform.Continuous
public
, I would have done so.
TransformBase
base class.
The abstract TransformBase
class presented as Listing 2 implements
the shared decorator functionality of the
next()
and hasNext()
methods
for most units implementing the Transform
interface.
However the essential convert()
method is left to
subclasses DiscreteDistributionTransform
and
ContinuousDistributionTransform
, whose
embedded Distribution
instances provide all important
quantile()
methods.
The TransformBase.next()
implementation
leverages convert()
to 'decorate' output from the installed
Driver
instance's next()
method.
TransformBase.hasNext()
simply delegates to
Driver.hasNext()
.
Even though the driver
field is accessible to all
TransformBase
methods, these methods instead rely on
TransformBase.getDriver()
,
which performs an initialization check.
© Charles Ames | Page created: 2022-08-29 | Last updated: 2022-08-29 |