package gdsc.smlm.search;
import java.util.Arrays;
import gdsc.core.utils.Maths;
/*-----------------------------------------------------------------------------
* 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.
*---------------------------------------------------------------------------*/
/**
* Specify the dimensions for a search
*/
public class SearchDimension implements Cloneable, Dimension
{
public final double min;
public final double max;
public final double minIncrement;
public final int nIncrement;
public final boolean active;
private double centre;
private double increment;
private double reduceFactor = 0.5;
private double[] values;
private boolean pad = false;
/**
* Instantiates a new inactive search dimension. The centre can be set to any value, the default is zero.
*/
public SearchDimension()
{
this(0);
}
/**
* Instantiates a new inactive search dimension. The centre can be set to any value.
*
* @param centre
* the centre
*/
public SearchDimension(double centre)
{
this(0, 0, 0, 1);
setCentre(centre);
}
/**
* Instantiates a new search dimension.
*
* @param min
* the minimum of the range
* @param max
* the maximum of the range
* @param minIncrement
* the min increment to use around the centre
* @param nIncrement
* the number of increments to use around the centre
*/
public SearchDimension(double min, double max, double minIncrement, int nIncrement)
{
this(min, max, minIncrement, nIncrement, min, max);
}
/**
* Instantiates a new search dimension.
*
* @param min
* the minimum of the range
* @param max
* the maximum of the range
* @param minIncrement
* the min increment to use around the centre
* @param nIncrement
* the number of increments to use around the centre
* @param lower
* the current lower bound of the range (will be clipped to min/max)
* @param upper
* the current upper bound of the range (will be clipped to min/max)
*/
public SearchDimension(double min, double max, double minIncrement, int nIncrement, double lower, double upper)
{
if (isInvalid(min))
throw new IllegalArgumentException("Min is not a valid number: " + min);
if (isInvalid(max))
throw new IllegalArgumentException("Max is not a valid number: " + max);
if (isInvalid(lower))
throw new IllegalArgumentException("Lower is not a valid number: " + lower);
if (isInvalid(upper))
throw new IllegalArgumentException("Upper is not a valid number: " + upper);
if (isInvalid(minIncrement))
throw new IllegalArgumentException("Min increment is not a valid number: " + minIncrement);
if (max < min)
throw new IllegalArgumentException("Max is less than min");
this.active = min < max;
if (active && nIncrement < 1)
throw new IllegalArgumentException("Steps must be more than 0: " + nIncrement);
if (minIncrement < 0)
throw new IllegalArgumentException("Min increment is negative: " + minIncrement);
if (upper < lower)
throw new IllegalArgumentException("Upper is less than lower");
// if (upper < min || upper > max)
// throw new IllegalArgumentException("Upper is outside min/max range");
// if (lower < min || lower > max)
// throw new IllegalArgumentException("Lower is outside min/max range");
// We round to the min increment so that the values returned should be identical if the centre is moved by a factor of the increment.
this.minIncrement = minIncrement;
this.min = round(min);
this.max = round(max);
this.nIncrement = nIncrement;
// Rounding changes the range so bring the upper and lower back within
lower = Maths.clip(this.min, this.max, lower);
upper = Maths.clip(this.min, this.max, upper);
setCentre((upper + lower) / 2);
setIncrement((upper - lower) / (2 * nIncrement));
}
/**
* Creates a new search dimension, respecting the current min/max and the increment settings. If the current search
* dimension is not active then an inactive dimension is returned centred between the lower and upper bounds.
*
* @param lower
* the current lower bound of the range
* @param upper
* the current upper bound of the range
* @return the search dimension
*/
public SearchDimension create(double lower, double upper)
{
if (!active)
return new SearchDimension((upper + lower) / 2);
if (lower < min)
lower = min;
if (upper > max)
upper = max;
return new SearchDimension(min, max, minIncrement, nIncrement, lower, upper);
}
/**
* Creates a new search dimension, respecting the current settings and changing the number of increments.
*
* @param nIncrement
* the number of increments to use around the centre
* @return the search dimension
*/
public SearchDimension create(int nIncrement)
{
return new SearchDimension(min, max, minIncrement, nIncrement, getLower(), getUpper());
}
private static boolean isInvalid(double d)
{
return Double.isNaN(d) || Double.isInfinite(d);
}
/**
* Round the value to the nearest min increment. If min increment is zero no rounding is performed.
*
* @param value
* the value
* @return the rounded value
*/
public double round(double value)
{
if (canRound())
return Maths.round(value, minIncrement);
return value;
}
/**
* If the dimension is not active or min increment is zero no rounding is performed.
*
* @see gdsc.smlm.search.Dimension#canRound()
*/
public boolean canRound()
{
return (active && minIncrement != 0);
}
/**
* Sets the centre.
*
* @param centre
* the new centre of the range in the dimension
*/
public void setCentre(double centre)
{
if (active && (centre < min || centre > max))
throw new IllegalArgumentException("Centre is outside min/max range");
this.centre = round(centre);
values = null;
}
/**
* Gets the centre of the range in the dimension
*
* @return the centre of the range in the dimension
*/
public double getCentre()
{
return centre;
}
/**
* Gets the increment.
*
* @return the increment
*/
public double getIncrement()
{
return increment;
}
/**
* Sets the increment.
*
* @param increment
* the new increment
*/
public void setIncrement(double increment)
{
if (increment < minIncrement)
increment = minIncrement;
this.increment = round(increment);
values = null;
}
/**
* Gets the current lower bound of the range
*
* @return the current lower bound of the range
*/
public double getLower()
{
return values()[0];
}
/**
* Gets the current upper bound of the range
*
* @return the current upper bound of the range
*/
public double getUpper()
{
values();
return values[values.length - 1];
}
/*
* (non-Javadoc)
*
* @see gdsc.smlm.search.Dimension#isAtBounds(double)
*/
public boolean isAtBounds(double v)
{
values();
return (v <= values[0] || v >= values[values.length - 1]);
}
/**
* Get the values of the dimension around the current centre using the configured increments.
* Note: If the values are outside the min/max range then by default the number of values may be reduced.
* <p>
* If the pad setting is enabled then the number of values should remain constant as the values are padded in the
* opposite direction.
*
* @return the values
*/
public double[] values()
{
if (values != null)
return values;
if (!active)
return values = new double[] { centre };
values = new double[getMaxLength()];
int size = 0;
double value = round(centre - nIncrement * increment);
if (value < min)
{
values[size++] = min;
// Avoid further values below the min
for (int i = nIncrement - 1; i >= 1; i--)
{
value = round(centre - i * increment);
if (value < min)
continue;
values[size++] = value;
}
if (centre != min)
values[size++] = centre;
}
else
{
// Not at the limit
for (int i = nIncrement; i >= 1; i--)
{
values[size++] = round(centre - i * increment);
}
values[size++] = centre; // Already rounded and within range
}
for (int i = 1; i <= nIncrement; i++)
{
value = round(centre + i * increment);
if (value > max)
{
if (centre != max)
values[size++] = max;
// Avoid further values outside the range
break;
}
values[size++] = value;
}
// double[] check = values.clone();
// Arrays.sort(check);
// for (int i=0; i<check.length; i++)
// if (check[i] != values[i])
// throw new RuntimeException("Not sorted");
// Check for duplicates if at the limits
if (size != values.length)
{
// Option to pad in the opposite direction
if (pad)
{
if (values[0] == min)
{
if (values[size - 1] != max)
{
// Pad up
for (int i = nIncrement + 1; size < values.length; i++)
{
value = round(centre + i * increment);
if (value > max)
{
values[size++] = max;
break;
}
values[size++] = value;
}
}
}
else
{
// Pad down
for (int i = nIncrement + 1; size < values.length; i++)
{
value = round(centre - i * increment);
if (value < min)
{
values[size++] = min;
break;
}
values[size++] = value;
}
// Simple option is to sort.
// A better option is to copy the values to the correct place.
Arrays.sort(values, 0, size);
}
// In case we could not pad enough
if (size != values.length)
values = Arrays.copyOf(values, size);
}
else
{
// No padding so truncate
values = Arrays.copyOf(values, size);
}
}
return values;
}
/**
* Gets the max length of the values array
*
* @return the max length
*/
public int getMaxLength()
{
return 2 * nIncrement + 1;
}
/**
* @return the reduceFactor
*/
public double getReduceFactor()
{
return reduceFactor;
}
/**
* Set the reduce factor. A value of 1 will prevent the range being reduced by the {@link #reduce()} method.
*
* @param reduceFactor
* the reduce factor (must be between 0 (exclusive) and 1 (inclusive))
*/
public void setReduceFactor(double reduceFactor)
{
if (reduceFactor <= 0 || reduceFactor > 1)
throw new IllegalArgumentException("Reduce factor must be between 0 and 1 (inclusive)");
this.reduceFactor = reduceFactor;
}
/**
* Reduce the size of the increment by multiplying by the reduce factor
*/
public void reduce()
{
setIncrement(increment * reduceFactor);
}
/**
* Checks if the {@link #reduce()} method will result in a change to the range returned by {@link #values()}
*
* @return true, if the range can be reduced
*/
public boolean canReduce()
{
return active && increment != minIncrement && reduceFactor < 1;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#clone()
*/
@Override
public SearchDimension clone()
{
try
{
return (SearchDimension) super.clone();
}
catch (CloneNotSupportedException e)
{
return null;
}
}
/**
* Checks if padding the values in the opposite direction when the range overlaps the min/max.
*
* @return true, if padding the values
*/
public boolean isPad()
{
return pad;
}
/**
* Set to true if padding the values in the opposite direction when the range overlaps the min/max
*
* @param pad
* true, if padding the values in the opposite direction when the range overlaps the min/max
*/
public void setPad(boolean pad)
{
this.pad = pad;
values = null;
}
/**
* Enumerate from the lower to the upper value using the configured nIncrement (n) to define the number of steps
* (2*n+1).
* <p>
* No range check is performed against the current min/max so the returned values can be outside the allowed range.
* The min increment setting is respected so the number of actual steps may be smaller.
*
* @param lower
* the lower
* @param upper
* the upper
* @return the double[]
*/
public double[] enumerate(double lower, double upper)
{
return enumerate(lower, upper, 2 * nIncrement + 1);
}
/**
* Enumerate from the lower to the upper value using the number of steps.
* <p>
* No range check is performed against the current min/max so the returned values can be outside the allowed range.
* The min increment setting is respected so the number of actual steps may be smaller.
*
* @param lower
* the lower
* @param upper
* the upper
* @param steps
* the steps
* @return the double[]
*/
public double[] enumerate(double lower, double upper, int steps)
{
if (upper <= lower || steps < 2)
return new double[] { round(lower) };
lower = round(lower);
upper = round(upper);
double inc = (upper - lower) / (steps - 1);
if (inc < minIncrement)
{
inc = minIncrement;
}
steps = 1 + (int) Math.ceil((upper - lower) / inc);
final double[] values = new double[steps];
int size = 0;
for (int i = 0; i < values.length; i++)
{
final double v = round(lower + i * inc);
if (v >= upper)
{
values[size++] = upper;
break;
}
values[size++] = v;
}
// Check
if (values[size - 1] != upper)
throw new RuntimeException("enumeration is invalid");
return (size != values.length) ? Arrays.copyOf(values, size) : values;
}
/*
* (non-Javadoc)
*
* @see gdsc.smlm.search.Dimension#getMin()
*/
public double getMin()
{
return min;
}
/*
* (non-Javadoc)
*
* @see gdsc.smlm.search.Dimension#getMax()
*/
public double getMax()
{
return max;
}
/*
* (non-Javadoc)
*
* @see gdsc.smlm.search.Dimension#isActive()
*/
public boolean isActive()
{
return active;
}
}