package io.vivarium.util;
import java.util.HashSet;
import java.util.Set;
import com.google.common.base.Preconditions;
public class Functions
{
/**
* computes the logit function (the inverse of the logistic sigmoid function) of a value.
*
* @param x
* @return logit(x)
*/
public static double logit(double x)
{
return Math.log(x / (1 - x));
}
/**
* computes the logistic sigmoid of a value, the logistic sigmoid is s(x) = 1 / ( 1 + e ^ -x )
*
* @param x
* @return sigmoid(x)
*/
public static double sigmoid(double x)
{
return 1 / (1 + Math.exp(-x));
}
/**
* computes the midpoint between two values on a logarithmic scale, defined as log((exp(A)+exp(B))/2), but is usable
* even when A or B are too large to fit into Java primitives.
*
* @param logA
* the log of A
* @param logB
* the log of B
* @return log((A+B)/2)
*/
public static double logarithmicAverage(double logA, double logB)
{
double difference = Math.abs(logA - logB);
double expandedDifference = Math.exp(difference);
return Math.log((expandedDifference + 1) / 2) + Math.min(logA, logB);
}
/**
* computes an ordered array such that each element in the array is as far as possible from all previous elements.
* In a grid search style application, this allows results to be generated at a low granularity, and over time to
* generate additional higher granularity data points.
*
* For example, the inputs of min = 1, max = 10, and steps = 10 will produce the output roughly equivalent to {1.0,
* 10.0, 5.0, 3.0, 7.0, 2.0, 4.0, 6.0, 8.0, 9.0}. The order of outputs is not guaranteed, but maximal gaps between
* preceeding values should always be observed.
*
* @param min
* The minimum value in the dither array. This will always be returned as the first element.
* @param max
* The maximum value in the dither array. This will always be returned as the second element.
* @param steps
* The total number of elements in the dither array. This value must be at least 2.
* @return The dither array.
*/
public static double[] generateDitherArray(double min, double max, int steps)
{
Preconditions.checkArgument(steps >= 2);
Preconditions.checkArgument(min < max);
// Build the basic data structures
double[] ditherArray = new double[steps];
double stepSize = (max - min) / (steps - 1);
int currentStepResolution = steps / 2;
// Immediately place the min value into the ditherArray and add it to the used values
Set<Integer> usedValues = new HashSet<>();
usedValues.add(0);
ditherArray[0] = min;
ditherArray[1] = max;
// Build the unusedValues, which should initially contain everything but value 0.
Set<Integer> unusedValues = new HashSet<>();
for (int i = 2; i < steps; i++)
{
unusedValues.add(i);
}
// Build the dither array, starting at the 3rd element (first and second element are always min and max)
int ditherArrayIndex = 2;
// While the resolution for steps is larger than 1, step based on step size from used values
while (currentStepResolution > 1)
{
// Track what we use for each pass based on step size, to move from unused to used values at the end
Set<Integer> newValues = new HashSet<>();
// For each used value, find the next value based on step size.
for (int usedValue : usedValues)
{
int newValue = usedValue + currentStepResolution;
newValues.add(newValue);
ditherArray[ditherArrayIndex++] = newValue * stepSize + min;
}
// After finding all new values, move all new values from unused to used values.
// Note: There is no need to check whether unusedValues contains any of the values we're seeing yet, because
// the step size shrinks faster than we can get collisions.
for (int newValue : newValues)
{
unusedValues.remove(newValue);
usedValues.add(newValue);
}
// At the end of the pass for this step size, halve the step size (rounding down).
currentStepResolution /= 2;
}
// After the step size resolution shrinks to 1, we just want to immediately go through all unused values and
// add them.
for (int unusedValue : unusedValues)
{
ditherArray[ditherArrayIndex++] = unusedValue * stepSize + min;
}
return ditherArray;
}
}