package gdsc.smlm.search;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import org.apache.commons.math3.random.HaltonSequenceGenerator;
import org.apache.commons.math3.random.RandomVectorGenerator;
import gdsc.core.logging.TrackProgress;
/*-----------------------------------------------------------------------------
* GDSC SMLM Software
*
* Copyright (C) 2016 Alex Herbert
* Genome Damage and Stability Centre
* University of Sussex, UK
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*---------------------------------------------------------------------------*/
/**
* Search a range of parameter space using a window divided into increments.
*/
public class SearchSpace
{
/** Used to ignore rounding values */
private static NonRoundingDimension nonRoundingDimension = new NonRoundingDimension();
private int iteration = 0;
private double[][] searchSpace;
private double[][] seed;
private double[][] scoredSearchSpace;
private ArrayList<String> scoredSearchSpaceHash = new ArrayList<String>();
private HashSet<String> coveredSpace = new HashSet<String>();
private StringBuilder sb = new StringBuilder();
// This introduces a dependency on another gdsc.smlm package
private TrackProgress tracker = null;
/**
* The refinement mode for the range search.
* <p>
* At each stage the refinement search enumerates a range for each dimension using the current interval. The optimum
* from this search space can be refined before the dimension range is reduced.
*/
public enum RefinementMode
{
/**
* No refinement before reducing the range
*/
NONE,
/**
* Enumerate each dimension in turn up to the next increment in either direction. This is iterated until the
* optimum converges and then the range is reduced.
*/
SINGLE_DIMENSION,
/**
* Enumerate all dimensions torgther up to the next increment in either direction. This is performed once and
* then the range is reduced.
*/
MULTI_DIMENSION
}
private RefinementMode searchMode = RefinementMode.SINGLE_DIMENSION;
/**
* Clone the search dimensions
*
* @param dimensions
* the search dimensions
* @return the clone
*/
public static SearchDimension[] clone(SearchDimension[] dimensions)
{
if (dimensions == null)
return null;
SearchDimension[] dimensions2 = new SearchDimension[dimensions.length];
for (int i = 0; i < dimensions.length; i++)
dimensions2[i] = dimensions[i].clone();
return dimensions2;
}
/**
* Search the configured search space until convergence of the optimum.
* <p>
* At each iteration the search will enumerate all points in the configured search space and find the optimum. If at
* the bounds of the range in any dimension then the range is re-centred and the process repeated. During repeats
* only those points that have not yet been evaluated will be passed to the score function. If not at the bounds
* then the range is re-centred and the width of the range reduced.
* <p>
* If a seed population was provided then the first step is to re-centre to the optimum of the seed population and
* the range refined/reduced as per the refinement mode parameter.
* <p>
* The process iterates until the range cannot be reduced in size, or convergence is reached. The input dimensions
* are modified during the search. Use the clone(SearchDimension[]) method to create a copy.
*
* @param <T>
* the type of comparable score
* @param dimensions
* the dimensions
* @param scoreFunction
* the score function
* @param checker
* the checker
* @param refinementMode
* The refinement mode for the range search.
* @return The optimum (or null)
*/
public <T extends Comparable<T>> SearchResult<T> search(SearchDimension[] dimensions,
ScoreFunction<T> scoreFunction, ConvergenceChecker<T> checker, RefinementMode refinementMode)
{
if (dimensions == null || dimensions.length == 0)
throw new IllegalArgumentException("Dimensions must not be empty");
if (scoreFunction == null)
throw new IllegalArgumentException("Score function is null");
reset();
// Find the best individual
SearchResult<T> current = findSeedOptimum(dimensions, scoreFunction);
if (current == null)
current = findOptimum(dimensions, scoreFunction, null);
SearchResult<T> previous = null;
boolean converged = false;
while (!converged)
{
iteration++;
previous = current;
if (!updateSearchSpace(dimensions, current, refinementMode))
break;
// Find the optimum and check convergence
current = findOptimum(dimensions, scoreFunction, current);
if (current == null)
break;
if (checker != null)
converged = checker.converged(previous, current);
}
if (tracker != null)
tracker.status("Converged [%d]", iteration);
// Free memory
scoredSearchSpace = null;
scoredSearchSpaceHash.clear();
coveredSpace.clear();
return current;
}
private void reset()
{
iteration = 0;
sb.setLength(0);
searchSpace = null;
scoredSearchSpace = null;
scoredSearchSpaceHash.clear();
coveredSpace.clear();
searchMode = RefinementMode.NONE;
}
/**
* Find the optimum in the configured search space.
*
* @param <T>
* the type of comparable score
* @param dimensions
* the dimensions
* @param scoreFunction
* the score function
* @return The optimum (or null)
*/
public <T extends Comparable<T>> SearchResult<T> findOptimum(SearchDimension[] dimensions,
ScoreFunction<T> scoreFunction)
{
if (dimensions == null || dimensions.length == 0)
throw new IllegalArgumentException("Dimensions must not be empty");
if (scoreFunction == null)
throw new IllegalArgumentException("Score function is null");
reset();
// Find the best individual
SearchResult<T> current = findOptimum(dimensions, scoreFunction, null);
return current;
}
/**
* Find the optimum in the seed population.
*
* @param <T>
* the type of comparable score
* @param scoreFunction
* the score function
* @return the new optimum
*/
private <T extends Comparable<T>> SearchResult<T> findSeedOptimum(SearchDimension[] dimensions,
ScoreFunction<T> scoreFunction)
{
if (!seedToSearchSpace(dimensions))
return null;
start("Find Seed Optimum");
scoredSearchSpace = searchSpace;
scoredSearchSpaceHash.clear();
SearchResult<T> optimum = scoreFunction.findOptimum(scoredSearchSpace);
// Re-centre on the seed
if (optimum != null)
{
final double[] p = optimum.point;
for (int i = 0; i < dimensions.length; i++)
{
dimensions[i].setCentre(p[i]);
// In-case the seed was not on the min interval grid
// Should not happen as we now map the seed to the min interval
//p[i] = dimensions[i].getCentre();
}
}
end();
return optimum;
}
private boolean seedToSearchSpace(Dimension[] dimensions)
{
if (seed == null)
return false;
// Get the limits of active dimensions
final int[] indices = new int[dimensions.length];
int size = 0;
final double[] min = new double[dimensions.length];
final double[] max = new double[dimensions.length];
for (int i = 0; i < dimensions.length; i++)
{
if (dimensions[i].isActive())
{
min[size] = dimensions[i].getMin();
max[size] = dimensions[i].getMax();
indices[size++] = i;
if (!dimensions[i].canRound())
dimensions[i] = nonRoundingDimension;
}
}
for (double[] p : seed)
{
// Check the seed has the correct dimensions
if (p == null || p.length != indices.length)
return false;
// Check the data is within the limits for active dimensions
for (int j = 0; j < size; j++)
{
// Round to dimension interval
final int k = indices[j];
final double value = dimensions[k].round(p[k]);
if (value < min[j])
return false;
else if (value > max[j])
return false;
p[k] = value;
}
}
searchSpace = seed;
return true;
}
/**
* Find the optimum. Create the search space using the current dimensions. Score any new point that has not
* previously been scored. Compare the result with the current optimum and return the best.
*
* @param <T>
* the type of comparable score
* @param scoreFunction
* the score function
* @param current
* the current optimum
* @return the new optimum
*/
private <T extends Comparable<T>> SearchResult<T> findOptimum(SearchDimension[] dimensions,
ScoreFunction<T> scoreFunction, SearchResult<T> current)
{
if (!createSearchSpace(dimensions, current))
return null;
start("Find Optimum");
scoredSearchSpace = searchSpace;
scoredSearchSpaceHash.clear();
if (!coveredSpace.isEmpty())
{
// Check we do not recompute scores
scoredSearchSpace = new double[searchSpace.length][];
scoredSearchSpaceHash.ensureCapacity(searchSpace.length);
int size = 0;
for (int i = 0; i < searchSpace.length; i++)
{
final String hash = generateHashString(searchSpace[i]);
if (!coveredSpace.contains(hash))
{
scoredSearchSpace[size++] = searchSpace[i];
scoredSearchSpaceHash.add(hash);
}
}
if (size == 0)
{
end();
// We have scored everything already so return the current best
return current;
}
//System.out.printf("Scoring %d / %d\n", size, searchSpace.length);
scoredSearchSpace = Arrays.copyOf(scoredSearchSpace, size);
}
SearchResult<T> optimum = scoreFunction.findOptimum(scoredSearchSpace);
if (optimum != null)
{
//System.out.printf("Optimum = %s\n", optimum.score);
//if (current != null)
// System.out.printf("Current = %s\n", current.score);
// Replace if better
if (optimum.compareTo(current) < 0)
{
current = optimum;
//if (searchMode == REFINE)
// System.out.printf("Refine improved = %s\n", current.score);
}
}
end();
return current;
}
/**
* Generate hash string.
*
* @param values
* the values
* @return the string
*/
private String generateHashString(double[] values)
{
for (int i = 0; i < values.length; i++)
sb.append(values[i]);
final String hash = sb.toString();
sb.setLength(0);
return hash;
}
/**
* Creates the search space.
*
* @param <T>
* the type of comparable score
* @param current
* the current
* @return true, if successful
*/
private <T extends Comparable<T>> boolean createSearchSpace(SearchDimension[] dimensions, SearchResult<T> current)
{
start("Create Search Space");
if (searchMode == RefinementMode.SINGLE_DIMENSION && current != null)
searchSpace = createRefineSpace(dimensions, current.point);
else if (searchMode == RefinementMode.MULTI_DIMENSION && current != null)
searchSpace = createBoundedSearchSpace(dimensions, current.point);
else
// Enumerate
searchSpace = createSearchSpace(dimensions);
end();
return searchSpace != null;
}
/**
* Creates the refine space.
*
* @param dimensions
* the dimensions
* @param point
* the point
* @return the double[][]
*/
public static double[][] createRefineSpace(SearchDimension[] dimensions, double[] point)
{
final double[][] dimensionValues = new double[dimensions.length][];
for (int i = 0; i < dimensions.length; i++)
{
double[] values = dimensions[i].values();
// Find the range values either side of the point value
double v = point[i];
double min = v;
double max = v;
for (int j = 0; j < values.length; j++)
{
if (values[j] < v)
min = values[j];
else if (values[j] > v)
{
max = values[j];
break;
}
}
// Create a sequence from min to max.
// Add option to configure the number of steps?
dimensionValues[i] = dimensions[i].enumerate(min, max, 100);
}
return createRefineSpace(dimensionValues, point);
}
/**
* Creates the refine space.
*
* @param dimensionValues
* the dimension values
* @param point
* the point
* @return the double[][]
*/
private static double[][] createRefineSpace(double[][] dimensionValues, double[] point)
{
// Get the values
int combinations = 0;
for (int i = 0; i < dimensionValues.length; i++)
{
combinations += dimensionValues[i].length;
}
// This will be a list of points enumerating the entire range
// of the dimensions
final double[][] searchSpace = new double[combinations][];
// Start with the min value in each dimension
int n = 0;
// Enumerate each dimension
for (int i = 0; i < dimensionValues.length; i++)
{
// Values to iterate over for this dimension
final double[] v1 = dimensionValues[i];
for (int k = 0; k < v1.length; k++)
{
// Create a new point with an updated value for this dimension
final double[] v3 = point.clone();
v3[i] = v1[k];
searchSpace[n++] = v3;
}
}
return searchSpace;
}
/**
* Creates the bounded search space.
*
* @param dimensions
* the dimensions
* @param point
* the point
* @return the double[][]
*/
public static double[][] createBoundedSearchSpace(SearchDimension[] dimensions, double[] point)
{
final double[][] dimensionValues = new double[dimensions.length][];
for (int i = 0; i < dimensions.length; i++)
{
double[] values = dimensions[i].values();
// Find the range values either side of the point value
double v = point[i];
double min = v;
double max = v;
for (int j = 0; j < values.length; j++)
{
if (values[j] < v)
min = values[j];
else if (values[j] > v)
{
max = values[j];
break;
}
}
// Create a sequence from min to max
dimensionValues[i] = dimensions[i].enumerate(min, max);
}
return createSearchSpace(dimensionValues);
}
/**
* Creates the search space.
*
* @param dimensions
* the dimensions
* @return the double[][]
*/
public static double[][] createSearchSpace(SearchDimension[] dimensions)
{
// Get the values
final double[][] dimensionValues = new double[dimensions.length][];
for (int i = 0; i < dimensions.length; i++)
{
dimensionValues[i] = dimensions[i].values();
//System.out.printf(" [%d] %.3f-%.3f", i, dimensions[i].getLower(), dimensions[i].getUpper());
}
//System.out.println();
return createSearchSpace(dimensionValues);
}
/**
* Count the number of combinations if enumerating the search space.
*
* @param dimensions
* the dimensions
* @return the number of combinations
*/
public static long countCombinations(SearchDimension[] dimensions)
{
long combinations = 1;
for (int i = 0; i < dimensions.length; i++)
{
combinations *= dimensions[i].values().length;
}
return combinations;
}
/**
* Creates the search space.
*
* @param dimensionValues
* the dimension values
* @return the double[][]
*/
private static double[][] createSearchSpace(double[][] dimensionValues)
{
// Get the values
int combinations = 1;
final double[] v = new double[dimensionValues.length];
for (int i = 0; i < dimensionValues.length; i++)
{
combinations *= dimensionValues[i].length;
v[i] = dimensionValues[i][0];
}
// This will be a list of points enumerating the entire range
// of the dimensions
final double[][] searchSpace = new double[combinations][];
// Start with the min value in each dimension
searchSpace[0] = v;
int n = 1;
try
{
// Enumerate each dimension
for (int i = 0; i < dimensionValues.length; i++)
{
// The number of current points
final int size = n;
// Values to iterate over for this dimension
final double[] v1 = dimensionValues[i];
// For all the current points
for (int j = 0; j < size; j++)
{
// The point values
final double[] v2 = searchSpace[j];
// We started with the min value for the dimension
// so go from index 1 upwards
for (int k = 1; k < v1.length; k++)
{
// Create a new point with an updated value for this dimension
final double[] v3 = v2.clone();
v3[i] = v1[k];
searchSpace[n++] = v3;
}
}
}
}
catch (ArrayIndexOutOfBoundsException e)
{
// Return false
e.printStackTrace();
n = -1;
}
return (n == combinations) ? searchSpace : null;
}
/**
* Update search space. Re-centre the dimension on the current optimum. If the current optimum is at the bounds of
* the dimensions then check if the bounds change when re-centring. If the bound change then the search must
* continue with the current dimension ranges. If the bounds do not change then the dimension ranges can be reduced.
*
* @param current
* the current optimum
* @return true, if successful
*/
private boolean updateSearchSpace(SearchDimension[] dimensions, SearchResult<?> current,
RefinementMode refinementMode)
{
if (current == null)
return false;
start("Update search space");
boolean changed = false;
final double[] p = current.point;
// System.out.printf("[%d] Before:", iteration);
// for (int i = 0; i < dimensions.length; i++)
// {
// if (dimensions[i].isActive())
// System.out.printf(" %.2f-%.2f (%.2f)", dimensions[i].getLower(), dimensions[i].getUpper(), p[i]);
// }
// System.out.println();
if (searchMode != RefinementMode.NONE)
{
// During refinement we will not repeat the same point if the optimum moves
coveredSpace.clear();
// Move to the centre using the current optimum
for (int i = 0; i < dimensions.length; i++)
{
if (p[i] != dimensions[i].getCentre())
{
changed = true;
dimensions[i].setCentre(p[i]);
}
}
if (searchMode == RefinementMode.SINGLE_DIMENSION)
{
if (!changed)
{
// Switch back to enumeration of a smaller range if
// the refinement has not changed the centre
searchMode = RefinementMode.NONE;
changed = reduceRange(dimensions);
}
}
else if (searchMode == RefinementMode.MULTI_DIMENSION)
{
// We enumerated all the points around the optimum so
// just switch back to enumeration of a smaller range
searchMode = RefinementMode.NONE;
changed = reduceRange(dimensions);
}
}
else
{
// searchMode == ENUMERATION
// Process each dimension
for (int i = 0; i < dimensions.length; i++)
{
// Check if at the bounds of the dimension values
final boolean atBounds = dimensions[i].isAtBounds(p[i]);
// Only if at the bounds then move the centre.
// (There is no point moving the bounds if not currently at the limits).
if (atBounds)
{
// Get the current bounds
final double[] values = dimensions[i].values();
// Move to the centre using the current optimum
dimensions[i].setCentre(p[i]);
// Check if the bounds have changed due to the move
if (changed(values, dimensions[i].values()))
changed = true;
}
}
if (changed)
{
// If changed then we stick to the current range.
// Store all the scored search space so we do not recompute it.
if (scoredSearchSpaceHash.isEmpty())
{
// Compute the hash for each item scored
for (int i = 0; i < scoredSearchSpace.length; i++)
coveredSpace.add(generateHashString(scoredSearchSpace[i]));
}
else
{
// Hash was already computed
for (int i = 0; i < scoredSearchSpaceHash.size(); i++)
coveredSpace.add(scoredSearchSpaceHash.get(i));
scoredSearchSpaceHash.clear();
}
}
else
{
// No changes at the current range.
// We can reduce/refine the search space.
// Move to the centre using the current optimum
for (int i = 0; i < dimensions.length; i++)
{
dimensions[i].setCentre(p[i]);
}
// Clear the memory of the space that has been searched
// (as the search space is about to be altered so the values may not overlap).
coveredSpace.clear();
// Optionally switch to refinement
if (refinementMode != RefinementMode.NONE)
{
searchMode = refinementMode;
changed = true;
}
else
{
changed = reduceRange(dimensions);
}
}
}
end();
// System.out.printf("[%d] After:", iteration);
// for (int i = 0; i < dimensions.length; i++)
// {
// if (dimensions[i].isActive())
// System.out.printf(" %.2f-%.2f (%.2f)", dimensions[i].getLower(), dimensions[i].getUpper(), p[i]);
// }
// System.out.println();
return changed;
}
/**
* Reduce range.
*
* @param dimensions
* the dimensions
* @return true, if successful
*/
private boolean reduceRange(SearchDimension[] dimensions)
{
// First check if the range can be reduced.
// If not then return false as nothing can be changed.
boolean reduced = false;
for (int i = 0; i < dimensions.length; i++)
{
if (dimensions[i].canReduce())
{
reduced = true;
break;
}
}
if (reduced)
{
// Reduce the range
for (int i = 0; i < dimensions.length; i++)
dimensions[i].reduce();
}
return reduced;
}
/**
* Check if the two arrays are different.
*
* @param v1
* the v 1
* @param v2
* the v 2
* @return true, if different
*/
private static boolean changed(double[] v1, double[] v2)
{
if (v1.length != v2.length)
return true;
for (int i = 0; i < v1.length; i++)
if (v1[i] != v2[i])
return true;
return false;
}
/**
* @return the tracker
*/
public TrackProgress getTracker()
{
return tracker;
}
/**
* Set a tracker to allow the progress to be followed
*
* @param tracker
* the tracker to set
*/
public void setTracker(TrackProgress tracker)
{
this.tracker = tracker;
}
/**
* Get the iteration. The iteration is increased each time the population grows as part of the [grow, evaluate,
* select] cycle.
*
* @return the iteration
*/
public int getIteration()
{
return iteration;
}
/**
* Send a start message to the tracker
*
* @param stage
* The stage that has started
*/
private void start(String stage)
{
if (tracker != null)
{
tracker.status(stage + " [%d]", iteration);
tracker.progress(0);
}
}
/**
* Send an end signal to the tracker
*/
private void end()
{
if (tracker != null)
tracker.progress(1);
}
/**
* Gets the current search space. This is the space that was most recently evaluated.
*
* @return the search space
*/
public double[][] getSearchSpace()
{
return searchSpace;
}
/**
* Search the configured search space until convergence of the optimum.
* <p>
* At each iteration the search will randomly sample points in the configured search space and score them. The top
* fraction of the results is used to redefine the search space (with padding). Note: The range cannot fall below
* the settings defined by the min increment and nIntervals for each of the input dimensions.
* <p>
* The process iterates until convergence is reached. The input dimensions are used to define the min/max and the
* current lower/upper bounds of the range. The dimensions are not modified.
*
* @param <T>
* the type of comparable score
* @param dimensions
* the dimensions
* @param scoreFunction
* the score function
* @param checker
* the checker
* @param samples
* the samples
* @param fraction
* the fraction
* @param padding
* the padding
* @return The optimum (or null)
*/
public <T extends Comparable<T>> SearchResult<T> enrichmentSearch(Dimension[] dimensions,
FullScoreFunction<T> scoreFunction, ConvergenceChecker<T> checker, int samples, double fraction,
double padding)
{
if (dimensions == null || dimensions.length == 0)
throw new IllegalArgumentException("Dimensions must not be empty");
if (scoreFunction == null)
throw new IllegalArgumentException("Score function is null");
if (fraction <= 0 || fraction >= 1)
throw new IllegalArgumentException("Fraction must be between 0 and 1");
if (samples < 1)
throw new IllegalArgumentException("Samples must be strictly positive");
if (padding < 0)
throw new IllegalArgumentException("Padding must be positive");
reset();
// Keep the same generator throughout
final HaltonSequenceGenerator[] generator = new HaltonSequenceGenerator[1];
// Find the best individual
SearchResult<T>[] scores = scoreSeed(dimensions, scoreFunction, samples, fraction, generator);
if (scores == null)
scores = score(dimensions, scoreFunction, samples, fraction, generator);
if (scores == null)
return null;
SearchResult<T> current = scores[0];
SearchResult<T> previous = null;
boolean converged = false;
while (!converged)
{
iteration++;
previous = current;
dimensions = updateSearchSpace(dimensions, current, scores, padding);
if (dimensions == null)
break;
// Find the optimum and check convergence
scores = score(dimensions, scoreFunction, samples, fraction, generator);
if (scores == null)
break;
SearchResult<T> optimum = scores[0];
if (optimum.compareTo(current) < 0)
current = optimum;
if (checker != null)
converged = checker.converged(previous, current);
}
if (tracker != null)
tracker.status("Converged [%d]", iteration);
return current;
}
/**
* Score the seed population and return the top fraction.
*
* @param <T>
* the type of comparable score
* @param dimensions
* the dimensions
* @param scoreFunction
* the score function
* @param samples
* the samples
* @param fraction
* the fraction
* @param generator
* the generator
* @return the score results
*/
private <T extends Comparable<T>> SearchResult<T>[] scoreSeed(Dimension[] dimensions,
FullScoreFunction<T> scoreFunction, int samples, double fraction, HaltonSequenceGenerator[] generator)
{
if (!seedToSearchSpace(dimensions))
return null;
// Pad search space with more samples
int remaining = samples - searchSpace.length;
if (remaining > 0)
{
double[][] sample = sample(dimensions, remaining, generator);
ArrayList<double[]> merged = new ArrayList<double[]>(sample.length + searchSpace.length);
merged.addAll(Arrays.asList(searchSpace));
merged.addAll(Arrays.asList(sample));
searchSpace = merged.toArray(new double[merged.size()][]);
}
// Score
SearchResult<T>[] scores = scoreFunction.score(searchSpace);
// Get the top fraction
int size = (int) Math.ceil(samples * fraction);
return scoreFunction.cut(scores, size);
}
/**
* Score random samples from the search space and return the top fraction.
*
* @param <T>
* the type of comparable score
* @param dimensions
* the dimensions
* @param scoreFunction
* the score function
* @param samples
* the samples
* @param fraction
* the fraction
* @param generator
* the generator
* @return the score results
*/
private <T extends Comparable<T>> SearchResult<T>[] score(Dimension[] dimensions,
FullScoreFunction<T> scoreFunction, int samples, double fraction, HaltonSequenceGenerator[] generator)
{
searchSpace = sample(dimensions, samples, generator);
// Score
SearchResult<T>[] scores = scoreFunction.score(searchSpace);
// Get the top fraction
int size = (int) Math.ceil(scores.length * fraction);
return scoreFunction.cut(scores, size);
}
/**
* Sample uniformly from the given dimensions. The lower and upper bounds of active dimensions are used to define
* the search space. The min interval of each dimension is respected.
* <p>
* If the input generator array is null, the first element is null, or the vector length is the wrong size then a
* new HaltonSequenceGenerator is used. Input an array of length 1 to obtain the default generator allowing calling
* the function again to generate a different search space.
*
* @param dimensions
* the dimensions
* @param samples
* the samples
* @param generator
* the generator
* @return the sample
*/
public static double[][] sample(Dimension[] dimensions, int samples, RandomVectorGenerator[] generator)
{
if (samples <= 0)
return null;
// Count the number of active dimensions
final int[] indices = new int[dimensions.length];
final Dimension[] dimensions2 = new Dimension[dimensions.length];
int size = 0;
final double[] centre = new double[dimensions.length];
final double[] lower = new double[dimensions.length];
final double[] range = new double[dimensions.length];
boolean requireRounding = false;
for (int i = 0; i < dimensions.length; i++)
{
centre[i] = dimensions[i].getCentre();
if (dimensions[i].isActive())
{
lower[size] = dimensions[i].getLower();
range[size] = (dimensions[i].getUpper() - lower[size]);
if (dimensions[i].canRound())
requireRounding = true;
else
// Ignore rounding functionality
dimensions[i] = nonRoundingDimension;
dimensions2[size] = dimensions[i];
indices[size++] = i;
}
}
if (requireRounding)
return sampleWithRounding(samples, generator, indices, dimensions2, size, centre, lower, range);
return sampleWithoutRounding(samples, generator, indices, size, centre, lower, range);
}
/**
* Sample with rounding. The input dimensions are used purely for rounding. The limits of the range have been
* precomputed.
*
* @param samples
* the samples
* @param generator
* the generator
* @param indices
* the indices
* @param dimensions
* the dimensions
* @param size
* the size
* @param centre
* the centre
* @param lower
* the lower
* @param range
* the range
* @return the sample
*/
private static double[][] sampleWithRounding(int samples, RandomVectorGenerator[] generator, final int[] indices,
final Dimension[] dimensions, int size, final double[] centre, final double[] lower, final double[] range)
{
RandomVectorGenerator g = createGenerator(generator, size);
// Generate random points
final double[][] searchSpace = new double[samples][];
for (int i = 0; i < samples; i++)
{
final double[] r = g.nextVector();
final double[] p = centre.clone();
for (int j = 0; j < size; j++)
{
// Ensure the min interval is respected
p[indices[j]] = dimensions[j].round(lower[j] + r[j] * range[j]);
}
searchSpace[i] = p;
}
return searchSpace;
}
private static RandomVectorGenerator createGenerator(RandomVectorGenerator[] generator, int size)
{
// Create the generator
if (generator == null)
generator = new RandomVectorGenerator[1];
RandomVectorGenerator g = generator[0];
if (g == null || g.nextVector().length != size)
{
generator[0] = g = new HaltonSequenceGenerator(size);
}
return g;
}
/**
* Sample uniformly from the given dimensions. The lower and upper bounds of active dimensions are used to define
* the search space. The min interval of each dimension is not respected, i.e the coordinates are not rounded.
* <p>
* If the input generator array is null, the first element is null, or the vector length is the wrong size then a
* new HaltonSequenceGenerator is used. Input an array of length 1 to obtain the default generator allowing calling
* the function again to generate a different search space.
*
* @param dimensions
* the dimensions
* @param samples
* the samples
* @param generator
* the generator
* @return the sample
*/
public static double[][] sampleWithoutRounding(Dimension[] dimensions, int samples,
RandomVectorGenerator[] generator)
{
if (samples <= 0)
return null;
// Count the number of active dimensions
final int[] indices = new int[dimensions.length];
final Dimension[] dimensions2 = new Dimension[dimensions.length];
int size = 0;
final double[] centre = new double[dimensions.length];
final double[] lower = new double[dimensions.length];
final double[] range = new double[dimensions.length];
for (int i = 0; i < dimensions.length; i++)
{
centre[i] = dimensions[i].getCentre();
if (dimensions[i].isActive())
{
lower[size] = dimensions[i].getLower();
range[size] = (dimensions[i].getUpper() - lower[size]);
dimensions2[size] = dimensions[i];
indices[size++] = i;
}
}
return sampleWithoutRounding(samples, generator, indices, size, centre, lower, range);
}
private static double[][] sampleWithoutRounding(int samples, RandomVectorGenerator[] generator, final int[] indices,
int size, final double[] centre, final double[] lower, final double[] range)
{
RandomVectorGenerator g = createGenerator(generator, size);
// Generate random points
final double[][] searchSpace = new double[samples][];
for (int i = 0; i < samples; i++)
{
final double[] r = g.nextVector();
final double[] p = centre.clone();
for (int j = 0; j < size; j++)
{
// Ensure the min interval is respected
p[indices[j]] = lower[j] + r[j] * range[j];
}
searchSpace[i] = p;
}
return searchSpace;
}
/**
* Update search space.
*
* @param <T>
* the type of comparable score
* @param dimensions
* the dimensions
* @param current
* the current
* @param scores
* the scores
* @param padding
* the padding
* @return the new dimensions
*/
private <T extends Comparable<T>> Dimension[] updateSearchSpace(Dimension[] dimensions, SearchResult<T> current,
SearchResult<T>[] scores, double padding)
{
// Find the limits in the current scores
final double[] lower = current.point.clone();
final double[] upper = current.point.clone();
for (int i = 0; i < scores.length; i++)
{
final double[] point = scores[i].point;
for (int j = 0; j < lower.length; j++)
{
if (lower[j] > point[j])
lower[j] = point[j];
if (upper[j] < point[j])
upper[j] = point[j];
}
}
// Pad the range
if (padding > 0)
{
for (int j = 0; j < lower.length; j++)
{
final double range = padding * (upper[j] - lower[j]);
lower[j] -= range;
upper[j] += range;
}
}
// Create new dimensions
for (int j = 0; j < lower.length; j++)
{
dimensions[j] = dimensions[j].create(lower[j], upper[j]);
}
return dimensions;
}
/**
* Seed the search with an initial population. This is used to determine the initial optimum and centre the search
* in the search space.
* <p>
* Note that the parameters in the seed are mapped to the min interval of the configured dimensions by rounding.
*
* @param seed
* the seed
*/
public void seed(double[][] seed)
{
this.seed = seed;
}
}