mirror of https://github.com/jlizier/jidt
Adding neighbour search utilities in preparation for attempting fast surrogate generation (performing multiple neighbour searches at once where we can); in particular adding a countPointsWithinR with a remapping of the point's indices. Unit test added as well for this.
This commit is contained in:
parent
0c5bed62d6
commit
1bb67f0840
|
|
@ -3128,6 +3128,136 @@ public class KdTree extends NearestNeighbourSearcher {
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int countPointsWithinR(int sampleIndex, double r,
|
||||||
|
boolean allowEqualToR, boolean[] additionalCriteria, int[] remapping) {
|
||||||
|
if (rootNode == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return countPointsWithinR(sampleIndex, rootNode, 0, r, allowEqualToR,
|
||||||
|
additionalCriteria, remapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the number of points within radius r of a given sample (sampleIndex),
|
||||||
|
* in the tree rooted at node (which is at the specified level in the tree),
|
||||||
|
* subject to an additional criteria,
|
||||||
|
* and the search points are reindexed according to the remapping specified in remapping.
|
||||||
|
*
|
||||||
|
* Nearest neighbour function to compare to r is a max norm between the
|
||||||
|
* high-level variables, with norm for each variable being the specified norm.
|
||||||
|
* (If {@link EuclideanUtils#NORM_EUCLIDEAN} was selected, then the supplied
|
||||||
|
* r should be the required Euclidean norm <b>squared</b>, since we switch it
|
||||||
|
* to {@link EuclideanUtils#NORM_EUCLIDEAN_SQUARED} internally).
|
||||||
|
*
|
||||||
|
* @param sampleIndex sample index in the data to find a nearest neighbour
|
||||||
|
* for
|
||||||
|
* @param node node to start searching from in the kd-tree. Cannot be null
|
||||||
|
* @param level which level we're currently at in the tree
|
||||||
|
* @param r radius within which to count points
|
||||||
|
* @param allowEqualToR if true, then count points at radius r also,
|
||||||
|
* otherwise only those strictly within r
|
||||||
|
* @param additionalCriteria array of booleans. Only count a point if it
|
||||||
|
* is within r and is true in additionalCrtieria.
|
||||||
|
* @param remapping array of time indices with which to remap the search points
|
||||||
|
* onto the same time index space as the additionalCriteria (this will
|
||||||
|
* apply to the supplied sampleIndex as well as other search points)
|
||||||
|
* @return count of points within r
|
||||||
|
*/
|
||||||
|
protected int countPointsWithinR(int sampleIndex,
|
||||||
|
KdTreeNode node, int level, double r, boolean allowEqualToR,
|
||||||
|
boolean[] additionalCriteria, int[] remapping) {
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
// Point to the correct array for the data at this level
|
||||||
|
int currentDim = level % totalDimensions;
|
||||||
|
double[][] data = dimensionToArray[currentDim];
|
||||||
|
int actualDim = dimensionToArrayIndex[currentDim];
|
||||||
|
|
||||||
|
// Check the distance on this particular dimension
|
||||||
|
double distOnThisDim = data[sampleIndex][actualDim] -
|
||||||
|
data[node.indexOfThisPoint][actualDim];
|
||||||
|
|
||||||
|
double absDistOnThisDim;
|
||||||
|
if (normTypeToUse == EuclideanUtils.NORM_MAX_NORM) {
|
||||||
|
absDistOnThisDim = (distOnThisDim > 0) ? distOnThisDim : - distOnThisDim;
|
||||||
|
} else {
|
||||||
|
// norm type is EuclideanUtils#NORM_EUCLIDEAN_SQUARED
|
||||||
|
// Track the square distance
|
||||||
|
absDistOnThisDim = distOnThisDim * distOnThisDim;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((node.indexOfThisPoint != sampleIndex) &&
|
||||||
|
((absDistOnThisDim < r) ||
|
||||||
|
( allowEqualToR && (absDistOnThisDim == r)))) {
|
||||||
|
// Preliminary check says we need to compute the full distance
|
||||||
|
// to use or at least to check if it should be counted.
|
||||||
|
if (additionalCriteria[remapping[node.indexOfThisPoint]]) {
|
||||||
|
// The additional criteria was ok, so continue:
|
||||||
|
boolean withinBounds = true;
|
||||||
|
for (int v = 0; v < originalDataSets.length; v++) {
|
||||||
|
// For each of our separate (multivariate) variables,
|
||||||
|
// compute the (specified) norm in that variable's space:
|
||||||
|
double distForVariableV;
|
||||||
|
// Distance calculation terminates early with Double.POSITIVE_INFINITY
|
||||||
|
// if it is clearly larger than r:
|
||||||
|
distForVariableV = normWithAbort(
|
||||||
|
originalDataSets[v][sampleIndex],
|
||||||
|
originalDataSets[v][node.indexOfThisPoint],
|
||||||
|
r, normTypeToUse);
|
||||||
|
if ((distForVariableV >= r) &&
|
||||||
|
!(allowEqualToR && (distForVariableV == r))) {
|
||||||
|
// We don't fit on this dimension, no point
|
||||||
|
// checking the others:
|
||||||
|
withinBounds = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (withinBounds) {
|
||||||
|
// This node gets counted
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KdTreeNode closestSubTree = null;
|
||||||
|
KdTreeNode furthestSubTree = null;
|
||||||
|
// And translate this to which subtree is closer
|
||||||
|
if (distOnThisDim < 0) {
|
||||||
|
// We need to search the left tree
|
||||||
|
closestSubTree = node.leftTree;
|
||||||
|
furthestSubTree = node.rightTree;
|
||||||
|
} else {
|
||||||
|
// We need to search the right tree
|
||||||
|
closestSubTree = node.rightTree;
|
||||||
|
furthestSubTree = node.leftTree;
|
||||||
|
}
|
||||||
|
// Update the search on that subtree
|
||||||
|
if (closestSubTree != null) {
|
||||||
|
count += countPointsWithinR(sampleIndex, closestSubTree,
|
||||||
|
level + 1, r, allowEqualToR, additionalCriteria, remapping);
|
||||||
|
}
|
||||||
|
if ((absDistOnThisDim < r) ||
|
||||||
|
( allowEqualToR && (distOnThisDim < 0) && (absDistOnThisDim == r))) {
|
||||||
|
// It's possible we could have a node within (or on) r
|
||||||
|
// in the other branch as well, so search there too.
|
||||||
|
// (Note: we only check furthest subtree in the == case
|
||||||
|
// when it's allowed
|
||||||
|
// *if* it's the right subtree, as only the right sub-tree
|
||||||
|
// can have node with distance in this coordinate *equal* to
|
||||||
|
// that of the current node -- left subtree must be strictly
|
||||||
|
// less than the coordinate of the current node, so
|
||||||
|
// distance to any of those points could not be equal.)
|
||||||
|
if (furthestSubTree != null) {
|
||||||
|
count += countPointsWithinR(sampleIndex, furthestSubTree,
|
||||||
|
level + 1, r, allowEqualToR, additionalCriteria, remapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int countPointsWithinR(double[][] sampleVectors, double r,
|
public int countPointsWithinR(double[][] sampleVectors, double r,
|
||||||
boolean allowEqualToR, boolean[] additionalCriteria) {
|
boolean allowEqualToR, boolean[] additionalCriteria) {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ public abstract class NearestNeighbourSearcher {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory method to construct the searcher from a set of double[][] data.
|
* Factory method to construct the searcher from a set of double[][] data.
|
||||||
|
* This will return a {@link KdTree} or if the data is univaraite
|
||||||
|
* (i.e. only one column) a {@link UnivariateNearestNeighbourSearcher}
|
||||||
*
|
*
|
||||||
* @param data a double[][] 2D data set, first indexed
|
* @param data a double[][] 2D data set, first indexed
|
||||||
* by time, second index by variable number.
|
* by time, second index by variable number.
|
||||||
|
|
@ -546,6 +548,29 @@ public abstract class NearestNeighbourSearcher {
|
||||||
public abstract int countPointsWithinR(int sampleIndex, double r,
|
public abstract int countPointsWithinR(int sampleIndex, double r,
|
||||||
boolean allowEqualToR, boolean[] additionalCriteria);
|
boolean allowEqualToR, boolean[] additionalCriteria);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As per {@link #countPointsWithinR(int, double, boolean)}
|
||||||
|
* however each point is subject to also meeting the additional
|
||||||
|
* criteria of being true in additionalCriteria,
|
||||||
|
* and the search points are reindexed according to the remapping
|
||||||
|
* specified in remapping
|
||||||
|
*
|
||||||
|
* @param sampleIndex sample index in the data to find a nearest neighbour
|
||||||
|
* for (already remapped if required)
|
||||||
|
* @param r radius within which to count points
|
||||||
|
* @param allowEqualToR if true, then count points at radius r also,
|
||||||
|
* otherwise only those strictly within r
|
||||||
|
* @param additionalCriteria array of booleans. Only count a point if it
|
||||||
|
* is within r and is true in additionalCrtieria.
|
||||||
|
* @param remapping array of time indices with which to remap the search points
|
||||||
|
* onto the same time index space as the additionalCriteria (this will
|
||||||
|
* apply to the supplied sampleIndex as well as other search points)
|
||||||
|
* @return the count of points within r.
|
||||||
|
*/
|
||||||
|
public abstract int countPointsWithinR(int sampleIndex, double r,
|
||||||
|
boolean allowEqualToR, boolean[] additionalCriteria,
|
||||||
|
int[] remapping);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* As per {@link #countPointsWithinR(double[][], double, boolean)}
|
* As per {@link #countPointsWithinR(double[][], double, boolean)}
|
||||||
* however each point is subject to also meeting the additional
|
* however each point is subject to also meeting the additional
|
||||||
|
|
|
||||||
|
|
@ -1000,6 +1000,60 @@ public class UnivariateNearestNeighbourSearcher extends NearestNeighbourSearcher
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int countPointsWithinR(int sampleIndex, double r, boolean allowEqualToR,
|
||||||
|
boolean[] additionalCriteria, int[] remapping) {
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
// Find where this node sits in the sorted array:
|
||||||
|
int indexInSortedArray = indicesInSortedArray[sampleIndex];
|
||||||
|
// Check the points with smaller data values first:
|
||||||
|
for (int i = indexInSortedArray - 1; i >= 0; i--) {
|
||||||
|
if (!additionalCriteria[remapping[sortedArrayIndices[i]]]) {
|
||||||
|
// This point failed the additional criteria,
|
||||||
|
// so skip checking it.
|
||||||
|
// We check this first, even though it's the norm
|
||||||
|
// that really determines whether we need to continue
|
||||||
|
// checking or not. In some circumstances this may be
|
||||||
|
// slower, but for our main application - KSG conditional MI -
|
||||||
|
// this should be faster.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double theNorm = norm(originalDataSet[sampleIndex],
|
||||||
|
originalDataSet[sortedArrayIndices[i]], normTypeToUse);
|
||||||
|
if ((allowEqualToR && (theNorm <= r)) ||
|
||||||
|
(!allowEqualToR && (theNorm < r))) {
|
||||||
|
count++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Else no point checking further points
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Next check the points with larger data values:
|
||||||
|
for (int i = indexInSortedArray + 1; i < numObservations; i++) {
|
||||||
|
if (!additionalCriteria[remapping[sortedArrayIndices[i]]]) {
|
||||||
|
// This point failed the additional criteria,
|
||||||
|
// so skip checking it.
|
||||||
|
// We check this first, even though it's the norm
|
||||||
|
// that really determines whether we need to continue
|
||||||
|
// checking or not. In some circumstances this may be
|
||||||
|
// slower, but for our main application - KSG conditional MI -
|
||||||
|
// this should be faster.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double theNorm = norm(originalDataSet[sampleIndex],
|
||||||
|
originalDataSet[sortedArrayIndices[i]], normTypeToUse);
|
||||||
|
if ((allowEqualToR && (theNorm <= r)) ||
|
||||||
|
(!allowEqualToR && (theNorm < r))) {
|
||||||
|
count++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Else no point checking further points
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* As per {@link #countPointsWithinR(int, double, boolean, boolean[])}
|
* As per {@link #countPointsWithinR(int, double, boolean, boolean[])}
|
||||||
* only for a specific data point.
|
* only for a specific data point.
|
||||||
|
|
|
||||||
|
|
@ -1396,6 +1396,95 @@ public class KdTreeTest extends TestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testAdditionalCriteriaWithRemapping() throws Exception {
|
||||||
|
// The following will include a little unit testing
|
||||||
|
// of UnivariateNearestNeighbourSearcher
|
||||||
|
// for the data0 in both cases, and the remaining data in
|
||||||
|
// the last:
|
||||||
|
validateAdditionalCriteriaWithRemapping(3, 1, false);
|
||||||
|
validateAdditionalCriteriaWithRemapping(2, 1, false);
|
||||||
|
// and now these will only test KdTree:
|
||||||
|
validateAdditionalCriteriaWithRemapping(3, 3, true);
|
||||||
|
validateAdditionalCriteriaWithRemapping(3, 3, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateAdditionalCriteriaWithRemapping(int variables,
|
||||||
|
int dimensionsPerVariable, boolean allowEqualsR) throws Exception {
|
||||||
|
int samples = 1000;
|
||||||
|
double[][][] data = new double[variables][][];
|
||||||
|
double[][][] dataAfter1Original = new double[variables-1][][];
|
||||||
|
for (int v = 0; v < variables; v++) {
|
||||||
|
data[v] = rg.generateNormalData(samples, dimensionsPerVariable, 0, 1);
|
||||||
|
if (v > 0) {
|
||||||
|
dataAfter1Original[v-1] = data[v];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double[][] data1 = data[0];
|
||||||
|
// Now shuffle dataAfter1 inside the data variable:
|
||||||
|
RandomGenerator rg = new RandomGenerator();
|
||||||
|
int[][] newOrderings = rg.generateRandomPerturbations(
|
||||||
|
samples, 1);
|
||||||
|
for (int v = 1; v < variables; v++) {
|
||||||
|
data[v] = MatrixUtils.extractSelectedTimePointsReusingArrays(data[v], newOrderings[0]);
|
||||||
|
}
|
||||||
|
// And store the reverse time index mapping (of the original time index for each data point in the 1 surrodate):
|
||||||
|
int[][] reverseMapping = new int[1][samples];
|
||||||
|
for (int n = 0; n < samples; n++) {
|
||||||
|
reverseMapping[0][newOrderings[0][n]] = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
long startTime = Calendar.getInstance().getTimeInMillis();
|
||||||
|
int[] dimensions = new int[variables];
|
||||||
|
for (int v = 0; v < variables; v++) {
|
||||||
|
dimensions[v] = dimensionsPerVariable;
|
||||||
|
}
|
||||||
|
int[] dimensionsAfter1 = new int[variables-1];
|
||||||
|
for (int v = 1; v < variables; v++) {
|
||||||
|
dimensionsAfter1[v-1] = dimensionsPerVariable;
|
||||||
|
}
|
||||||
|
KdTree kdTree = new KdTree(dimensions, data);
|
||||||
|
NearestNeighbourSearcher nnSearcherExceptVar1 =
|
||||||
|
NearestNeighbourSearcher.create(dimensionsAfter1, dataAfter1Original);
|
||||||
|
NearestNeighbourSearcher nnSearcherVar1 =
|
||||||
|
NearestNeighbourSearcher.create(data1);
|
||||||
|
long endTimeTree = Calendar.getInstance().getTimeInMillis();
|
||||||
|
System.out.printf("Additional criteria test: Tree of %d points constructed in : %.3f sec\n",
|
||||||
|
samples, ((double) (endTimeTree - startTime)/1000.0));
|
||||||
|
|
||||||
|
double[] r1 = {1.0};
|
||||||
|
boolean[] isWithinR = new boolean[samples];
|
||||||
|
int[] indicesWithinR = new int[samples+1];
|
||||||
|
|
||||||
|
for (int i = 0; i < r1.length; i++) {
|
||||||
|
for (int t = 0; t < samples; t++) {
|
||||||
|
// Establish our ground truth, including the shuffled variables after 1:
|
||||||
|
int count = kdTree.countPointsWithinR(t, r1[i], allowEqualsR);
|
||||||
|
// Alternative if we need to debug:
|
||||||
|
// Collection<NeighbourNodeData> coll = kdTree.findPointsWithinR(t, r1[i], allowEqualsR);
|
||||||
|
// int count = coll.size();
|
||||||
|
|
||||||
|
// Now search in variable 1 first, then use that
|
||||||
|
// to help the others:
|
||||||
|
nnSearcherVar1.findPointsWithinR(t, r1[i], allowEqualsR,
|
||||||
|
isWithinR, indicesWithinR);
|
||||||
|
int countUsingVar1 = kdTree.countPointsWithinR(t, r1[i],
|
||||||
|
allowEqualsR, 0, isWithinR);
|
||||||
|
assertEquals(count, countUsingVar1);
|
||||||
|
|
||||||
|
// And then search the kdTree which does not have variable 1, unshuffled but providing the reverse mapping:
|
||||||
|
int countAdditionalCriteria =
|
||||||
|
nnSearcherExceptVar1.countPointsWithinR(newOrderings[0][t], r1[i],
|
||||||
|
allowEqualsR, isWithinR, reverseMapping[0]);
|
||||||
|
assertEquals(count, countAdditionalCriteria);
|
||||||
|
|
||||||
|
// Finally, reset our boolean array:
|
||||||
|
for (int t2 = 0; indicesWithinR[t2] != -1; t2++) {
|
||||||
|
isWithinR[indicesWithinR[t2]] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void testPreviouslyTestedVariableMethodMultipleRs() throws Exception {
|
public void testPreviouslyTestedVariableMethodMultipleRs() throws Exception {
|
||||||
|
|
||||||
for (int vars = 2; vars <= 3; vars++) {
|
for (int vars = 2; vars <= 3; vars++) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue