diff --git a/src/junit/core/TestKDTree.java b/src/junit/core/TestKDTree.java new file mode 100644 index 0000000..98919b3 --- /dev/null +++ b/src/junit/core/TestKDTree.java @@ -0,0 +1,612 @@ +/** + * %SVN.HEADER% + */ +package junit.core; + +import net.sf.javaml.core.kdtree.KDTree; +import org.junit.Assert; +import org.junit.Test; +import java.util.Map; +import java.util.NoSuchElementException; + +public class TestKDTree { + static final double[] TEST_POINT_3D_1 = new double[]{ 1, 2, 3 }; + static final double[] TEST_POINT_3D_2 = new double[]{ -1, -2, -3 }; + static final double[] TEST_POINT_3D_3 = new double[]{ 15, 10, -3 }; + static final double[] TEST_POINT_3D_4 = new double[]{ 10, 0, 0 }; + static final double[] TEST_POINT_3D_MISSING = new double[]{ 0, -2, -3 }; + + static final double[] TEST_RANGE_3D_1 = new double[]{ 0, 0, 0 }; + static final double[] TEST_RANGE_3D_2 = new double[]{ 20, 20, 20 }; + + static final double[] TEST_POINT_2D_1 = new double[]{ 1, 2 }; + static final double[] TEST_POINT_2D_2 = new double[]{ -1, -2 }; + + static final double[] TEST_POINT_1D_1 = new double[]{ 2.2 }; + static final double[] TEST_POINT_1D_2 = new double[]{ -2.2 }; + static final double[] TEST_POINT_1D_3 = new double[]{ -3.3 }; + static final double[] TEST_POINT_1D_4 = new double[]{ 5.5 }; + static final double[] TEST_POINT_1D_MISSING = new double[]{ -1 }; // Nearest to TEST_POINT_1D_2 + + static final String TEST_OBJECT_1 = "TEST 1"; + static final String TEST_OBJECT_2 = "TEST 2"; + static final String TEST_OBJECT_3 = "TEST 3"; + + + /***************** + * TREE CREATION * + *****************/ + + @Test + public void testKDTree_creationSuccess() { + KDTree testTreeA = new KDTree(1); + Assert.assertNotNull(testTreeA); + + KDTree testTreeB = new KDTree(3); + Assert.assertNotNull(testTreeB); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_creationFailureZero() { + new KDTree(0); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_creationFailureNegative() { + new KDTree(-1); + } + + + /****************** + * TREE INSERTION * + ******************/ + + @Test + public void testKDTree_canAdd3DData() { + // Can add an item + KDTree testTreeA = new KDTree(3); + testTreeA.insert(TEST_POINT_3D_1, TEST_OBJECT_1); + Assert.assertEquals(testTreeA.size(), 1); + + // Add same point again, should be no change in count + testTreeA.insert(TEST_POINT_3D_1, TEST_OBJECT_1); + Assert.assertEquals(testTreeA.size(), 1); + + // Add more points + testTreeA.insert(TEST_POINT_3D_2, TEST_OBJECT_2); + testTreeA.insert(TEST_POINT_3D_3, TEST_OBJECT_3); + testTreeA.insert(TEST_POINT_3D_4, TEST_OBJECT_3); // With repeat value + Assert.assertEquals(testTreeA.size(), 4); + } + + @Test + public void testKDTree_canAdd2DData() { + // Can add an item + KDTree testTreeA = new KDTree(2); + testTreeA.insert(TEST_POINT_2D_1, TEST_OBJECT_1); + testTreeA.insert(TEST_POINT_2D_2, TEST_OBJECT_2); + Assert.assertEquals(testTreeA.size(), 2); + + // Add same point again, should be no change in count + testTreeA.insert(TEST_POINT_2D_1, TEST_OBJECT_3); + Assert.assertEquals(testTreeA.size(), 2); + } + + @Test + public void testKDTree_canAdd1DData() { + // Can add an item + KDTree testTreeA = new KDTree(1); + testTreeA.insert(TEST_POINT_1D_1, TEST_OBJECT_1); + testTreeA.insert(TEST_POINT_1D_2, TEST_OBJECT_2); + Assert.assertEquals(testTreeA.size(), 2); + + // Add same point again, should be no change in count + testTreeA.insert(TEST_POINT_1D_1, TEST_OBJECT_3); + Assert.assertEquals(testTreeA.size(), 2); + } + + @Test + public void testKDTree_canAddNullData() { + // Can add an item + KDTree testTreeA = new KDTree(1); + testTreeA.insert(TEST_POINT_1D_1, null); + testTreeA.insert(TEST_POINT_1D_2, null); + Assert.assertEquals(testTreeA.size(), 2); + + // Add same point again, should be no change in count + testTreeA.insert(TEST_POINT_1D_1, null); + Assert.assertEquals(testTreeA.size(), 2); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotAddMixedKeys() { + // Attempt to add 2D to a 3D tree + KDTree testTreeA = new KDTree(3); + testTreeA.insert(TEST_POINT_3D_1, TEST_OBJECT_1); + testTreeA.insert(TEST_POINT_2D_1, TEST_OBJECT_2); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotAddNullKey() { + // Attempt to add null to a 3D tree + KDTree testTreeA = new KDTree(3); + testTreeA.insert(null, TEST_OBJECT_1); + } + + + /****************** + * TREE SEARCHING * + ******************/ + + @Test + public void testKDTree_canSearchByPresentKey3D() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Get by valid key + Object result = testTreeA.search(TEST_POINT_3D_2); + Assert.assertTrue(result instanceof String); + Assert.assertEquals(result, TEST_OBJECT_2); + } + + @Test + public void testKDTree_canSearchByMissingKey3D() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Get by missing valid key + Object result = testTreeA.search(TEST_POINT_3D_MISSING); + Assert.assertNull(result); + } + + @Test + public void testKDTree_canSearchByPresentKey1D() { + KDTree testTreeA = createTreeWithTestData1D(); + + // Get by valid key + Object result = testTreeA.search(TEST_POINT_1D_3); + Assert.assertTrue(result instanceof String); + Assert.assertEquals(result, TEST_OBJECT_3); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotSearchInvalidKey() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by invalid key (wrong size) + testTreeA.search(TEST_POINT_2D_1); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotSearchNullKey() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by invalid key (null) + testTreeA.search(null); + } + + + /***************** + * TREE DELETING * + *****************/ + + @Test + public void testKDTree_canDeletePresentKey3D() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Delete data, check it's gone + testTreeA.delete(TEST_POINT_3D_2); + Object result = testTreeA.search(TEST_POINT_3D_2); + Assert.assertNull(result); + } + + @Test + public void testKDTree_canDeletePresentKey1D() { + KDTree testTreeA = createTreeWithTestData1D(); + + // Delete data, check it's gone + testTreeA.delete(TEST_POINT_1D_2); + Object result = testTreeA.search(TEST_POINT_1D_2); + Assert.assertNull(result); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotDeleteInvalidKey() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by invalid key (wrong size) + testTreeA.delete(TEST_POINT_2D_1); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotDeleteNullKey() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by invalid key (null) + testTreeA.delete(null); + } + + @Test(expected = NoSuchElementException.class) + public void testKDTree_cannotDeleteByMissingKey() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by missing key + testTreeA.delete(TEST_POINT_3D_MISSING); + } + + + /***************************** + * TREE NEAREST - SINGLE ARG * + *****************************/ + + @Test + public void testKDTree_canGetNearestWithPresentKey3D() { + KDTree testTreeA = createTreeWithTestData3D(); + + Object result = testTreeA.nearest(TEST_POINT_3D_MISSING); + Assert.assertTrue(result instanceof String); + Assert.assertEquals(result, TEST_OBJECT_2); // from TEST_POINT_3D_2 + } + + @Test + public void testKDTree_canGetNearestWithPresentKey1D() { + KDTree testTreeA = createTreeWithTestData1D(); + + Object result = testTreeA.nearest(TEST_POINT_1D_MISSING); + Assert.assertTrue(result instanceof String); + Assert.assertEquals(result, TEST_OBJECT_2); // from TEST_POINT_1D_2 + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotGetNearestWithInvalidKey() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by invalid key (wrong size) + testTreeA.nearest(TEST_POINT_2D_1); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotGetNearestWithNullKey() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by invalid key (null) + testTreeA.nearest(null); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testKDTree_cannotGetNearestWithLackOfData() { + KDTree testTreeA = new KDTree(3); + + // Search by missing key + testTreeA.nearest(TEST_POINT_3D_1); + } + + + /******************************* + * TREE NEAREST - MULTIPLE ARG * + *******************************/ + + @Test + public void testKDTree_canGetNearest2WithPresentKey3D() { + KDTree testTreeA = createTreeWithTestData3D(); + + Object[] result = testTreeA.nearest(TEST_POINT_3D_MISSING, 2); + Assert.assertTrue(result[0] instanceof String); + Assert.assertTrue(result[1] instanceof String); + Assert.assertEquals(result[0], TEST_OBJECT_2); // from TEST_POINT_3D_2 + Assert.assertEquals(result[1], TEST_OBJECT_1); // from TEST_POINT_3D_1 + } + + @Test + public void testKDTree_canGetNearest2WithPresentKey1D() { + KDTree testTreeA = createTreeWithTestData1D(); + + Object[] result = testTreeA.nearest(TEST_POINT_1D_MISSING, 3); + Assert.assertTrue(result[0] instanceof String); + Assert.assertTrue(result[1] instanceof String); + Assert.assertTrue(result[2] instanceof String); + Assert.assertEquals(result[0], TEST_OBJECT_2); // from TEST_POINT_1D_2 + Assert.assertEquals(result[1], TEST_OBJECT_3); // from TEST_POINT_1D_3 + Assert.assertEquals(result[2], TEST_OBJECT_1); // from TEST_POINT_1D_1 + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotGetNearest2WithInvalidKey() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by invalid key (wrong size) + testTreeA.nearest(TEST_POINT_2D_1, 2); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotGetNearest2WithNullKey() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by invalid key (null) + testTreeA.nearest(null, 2); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testKDTree_cannotGetNearest2WithLackOfData() { + KDTree testTreeA = new KDTree(3); + + // No data in the tree + testTreeA.nearest(TEST_POINT_3D_1, 2); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testKDTree_cannotGetNearestWithInvalidCount() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by missing key + testTreeA.nearest(TEST_POINT_3D_1, 1000); + } + + + /********************* + * TREE NEAREST KEYS * + *********************/ + + @Test + public void testKDTree_canGetNearestAsKeys() { + KDTree testTreeA = createTreeWithTestData3D(); + + double[][] results = testTreeA.nearestKeys(TEST_POINT_3D_1, 1); + Assert.assertNotNull(results); + Assert.assertTrue(results.length > 0); + Assert.assertArrayEquals(results[0], TEST_POINT_3D_1, 0.0); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotGetNearestKeysWithInvalidKey() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by invalid key (wrong size) + testTreeA.nearestKeys(TEST_POINT_2D_1, 1); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotGetNearestKeysWithNullKey() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by invalid key (null) + testTreeA.nearestKeys(null, 1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testKDTree_cannotGetNearestKeysWithLackOfData() { + KDTree testTreeA = new KDTree(3); + + // Search by missing key + testTreeA.nearestKeys(TEST_POINT_3D_1, 1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testKDTree_cannotGetNearestKeysWithInvalidCount() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by missing key + testTreeA.nearestKeys(TEST_POINT_3D_1, 1000); + } + + + /************** + * TREE RANGE * + **************/ + + @Test + public void testKDTree_canRangeSearch3D() { + KDTree testTreeA = createTreeWithTestData3D(); + + Object[] result = testTreeA.range(TEST_RANGE_3D_1, TEST_RANGE_3D_2); + Assert.assertEquals(result.length, 2); + Assert.assertTrue(result[0] instanceof String); + Assert.assertTrue(result[1] instanceof String); + Assert.assertEquals(result[0], TEST_OBJECT_1); // from TEST_POINT_3D_1 + Assert.assertEquals(result[1], TEST_OBJECT_3); // from TEST_POINT_3D_4 + } + + @Test + public void testKDTree_canRangeSearch1D() { + KDTree testTreeA = createTreeWithTestData1D(); + + Object[] result = testTreeA.range( + TEST_POINT_1D_2, + TEST_POINT_1D_4 + ); + Assert.assertEquals(result.length, 3); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotRangeSearchWithInvalidKeyDiff() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by invalid key (wrong size) + testTreeA.range(TEST_POINT_2D_1, TEST_RANGE_3D_2); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotRangeSearchWithInvalidKeyWrong() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by invalid key (wrong size) + testTreeA.range(TEST_POINT_2D_1, TEST_POINT_2D_1); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotRangeSearchWithInvalidKeyNullLeft() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by invalid key (null) + testTreeA.range(null, TEST_RANGE_3D_2); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_cannotRangeSearchWithInvalidKeyNullRight() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Search by invalid key (null) + testTreeA.range(TEST_RANGE_3D_2, null); + } + + @Test + public void testKDTree_canRangeSearchWithLackOfData() { + KDTree testTreeA = new KDTree(3); + Object[] result = testTreeA.range(TEST_RANGE_3D_1, TEST_RANGE_3D_2); + Assert.assertEquals(result.length, 0); + } + + + /************** + * TREE UTILS * + **************/ + + @Test + public void testKDTree_canGetTreeSize() { + KDTree testTreeA = createTreeWithTestData3D(); + Assert.assertEquals(testTreeA.size(), 4); + + KDTree testTreeB = new KDTree(3); + Assert.assertEquals(testTreeB.size(), 0); + } + + @Test + public void testKDTree_canGetTreeDimensions() { + KDTree testTreeA = createTreeWithTestData3D(); + Assert.assertEquals(testTreeA.dimensions(), 3); + + KDTree testTreeB = createTreeWithTestData1D(); + Assert.assertEquals(testTreeB.dimensions(), 1); + } + + @Test + public void testKDTree_canGetTreeAsString() { + KDTree testTreeA = createTreeWithTestData3D(); + Assert.assertTrue(testTreeA.toString().length() > 4); + + KDTree testTreeB = new KDTree(3); + Assert.assertEquals(testTreeB.toString(), "null"); + } + + @Test + public void testKDTree_canGetTreeAsMap() { + KDTree testTreeA = createTreeWithTestData3D(); + Map testTreeMap = testTreeA.toMap(); + Assert.assertEquals(testTreeMap.size(), 4); + + testTreeA.delete(TEST_POINT_3D_3); + testTreeMap = testTreeA.toMap(); + Assert.assertEquals(testTreeMap.size(), 3); + } + + @Test + public void testKDTree_canGetTreeAsMapWithLackOfData() { + KDTree testTreeA = new KDTree(3); + Map testTreeMap = testTreeA.toMap(); + Assert.assertNull(testTreeMap); + } + + + /*************** + * COMBO TESTS * + ***************/ + + @Test + public void testKDTree_canUpdateData() { + // Can add an item + KDTree testTreeA = createTreeWithTestData3D(); + Assert.assertEquals(testTreeA.search(TEST_POINT_3D_1), TEST_OBJECT_1); + Assert.assertEquals(testTreeA.size(), 4); + + testTreeA.insert(TEST_POINT_3D_1, TEST_OBJECT_3); + Assert.assertEquals(testTreeA.search(TEST_POINT_3D_1), TEST_OBJECT_3); + Assert.assertEquals(testTreeA.size(), 4); + } + + @Test + public void testKDTree_sizeIsCorrect() { + KDTree testTreeA = createTreeWithTestData3D(); + Assert.assertEquals(testTreeA.size(), 4); + + // Delete data, check size + testTreeA.delete(TEST_POINT_3D_1); + Assert.assertEquals(testTreeA.size(), 3); + + testTreeA.insert(TEST_POINT_3D_1, TEST_OBJECT_1); + Assert.assertEquals(testTreeA.size(), 4); + + testTreeA.insert(TEST_POINT_3D_MISSING, TEST_OBJECT_2); + Assert.assertEquals(testTreeA.size(), 5); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testKDTree_cannotSearchAfterEmptying() { + KDTree testTreeA = createTreeWithTestData3D(); + + // Delete all points + testTreeA.delete(TEST_POINT_3D_1); + testTreeA.delete(TEST_POINT_3D_2); + testTreeA.delete(TEST_POINT_3D_3); + testTreeA.delete(TEST_POINT_3D_4); + + // Search while data is supposedly empty + testTreeA.nearest(TEST_POINT_3D_1); + } + + + @Test + public void testKDTree_smokeTest() { + KDTree testTreeA = new KDTree(3); + testTreeA.insert(new double[]{ 1, 2, 3 }, TEST_OBJECT_1); + testTreeA.insert(new double[]{ -1, 2, 3 }, TEST_OBJECT_2); + testTreeA.insert(new double[]{ 1, -2, 3 }, TEST_OBJECT_3); + testTreeA.insert(new double[]{ 10, 20, -3 }, TEST_OBJECT_3); // With repeat value + testTreeA.insert(new double[]{ 12, 2.5, -34 }, TEST_OBJECT_1); + testTreeA.insert(new double[]{ 0, 0, 0 }, TEST_OBJECT_2); + testTreeA.insert(new double[]{ -0.5, 2, 3 }, TEST_OBJECT_3); + + String result = (String) testTreeA.search(new double[]{ 1, 2, 3 }); + Assert.assertEquals(result, TEST_OBJECT_1); + Assert.assertEquals(testTreeA.size(), 7); + + testTreeA.insert(new double[]{ 1, 2, 3 }, TEST_OBJECT_3); // With repeat key + String result2 = (String) testTreeA.search(new double[]{ 1, 2, 3 }); + Assert.assertEquals(result2, TEST_OBJECT_3); + Assert.assertEquals(testTreeA.size(), 7); + + testTreeA.delete(new double[]{ 1, 2, 3 }); + String result4 = (String) testTreeA.search(new double[]{ 1, 2, 3 }); + Assert.assertNull(result4); + Assert.assertEquals(testTreeA.size(), 6); + + String result5 = + (String) testTreeA.nearest(new double[]{ -0.1, -0.1, -0.1 }); + Assert.assertEquals(result5, TEST_OBJECT_2); + + testTreeA.insert(new double[]{ -0.1, -0.1, -0.1 }, TEST_OBJECT_1); + String result6 = + (String) testTreeA.nearest(new double[]{ -0.1, -0.1, -0.1 }); + Assert.assertEquals(result6, TEST_OBJECT_1); + } + + + /** Private test helpers */ + private KDTree createTreeWithTestData3D() { + KDTree testTreeA = new KDTree(3); + testTreeA.insert(TEST_POINT_3D_1, TEST_OBJECT_2); + testTreeA.insert(TEST_POINT_3D_1, TEST_OBJECT_1); // With repeat key, updated value + testTreeA.insert(TEST_POINT_3D_2, TEST_OBJECT_2); + testTreeA.insert(TEST_POINT_3D_3, TEST_OBJECT_3); + testTreeA.insert(TEST_POINT_3D_4, TEST_OBJECT_3); // With repeat value + + return testTreeA; + } + + private KDTree createTreeWithTestData1D() { + KDTree testTreeA = new KDTree(1); + testTreeA.insert(TEST_POINT_1D_1, TEST_OBJECT_1); + testTreeA.insert(TEST_POINT_1D_2, TEST_OBJECT_2); + testTreeA.insert(TEST_POINT_1D_3, TEST_OBJECT_3); + testTreeA.insert(TEST_POINT_1D_4, TEST_OBJECT_3); // With repeat value + + return testTreeA; + } +} diff --git a/src/junit/core/TestTypedKDTree.java b/src/junit/core/TestTypedKDTree.java new file mode 100644 index 0000000..d129630 --- /dev/null +++ b/src/junit/core/TestTypedKDTree.java @@ -0,0 +1,737 @@ +/** + * %SVN.HEADER% + */ +package junit.core; + +import net.sf.javaml.core.kdtree.KDTree; +import net.sf.javaml.core.kdtree.TypedKDTree; +import org.junit.Assert; +import org.junit.Test; +import java.util.*; + +public class TestTypedKDTree { + // Implementation for 1D points (not provided by TypedKDTree) + static class Point1D implements TypedKDTree.Point { + private final double[] point; + public Point1D(double x) { point = new double[]{ x }; } + public Point1D(double x, boolean invalidPoint) { + point = invalidPoint ? new double[]{ x, x } : new double[]{ x }; + } + @Override + public double[] getKDTreePoint() { return point; } + } + static class Point2D implements TypedKDTree.Point { + private final double[] point; + public Point2D(double x, double y) { + point = new double[]{ x, y }; + } + @Override + public double[] getKDTreePoint() { return point; } + } + static class Point3D implements TypedKDTree.Point { + private final double[] point; + public Point3D(double x, double y, double z) { + point = new double[]{ x, y, z }; + } + @Override + public double[] getKDTreePoint() { return point; } + } + + // Point3D and Point2D are provided for convenience by TypedKDTree + static final Point3D TEST_POINT_3D_1 = new Point3D(1, 2, 3); + static final Point3D TEST_POINT_3D_2 = new Point3D(-1, -2, -3); + static final Point3D TEST_POINT_3D_3 = new Point3D(15, 10, -3); + static final Point3D TEST_POINT_3D_4 = new Point3D(10, 0, 0); + static final Point3D TEST_POINT_3D_MISSING = new Point3D(0, -2, -3); + + static final Point3D TEST_RANGE_3D_1 = new Point3D(0, 0, 0); + static final Point3D TEST_RANGE_3D_2 = new Point3D(20, 20, 20); + + static final Point2D TEST_POINT_2D_1 = new Point2D(1, 2); + static final Point2D TEST_POINT_2D_2 = new Point2D(-1, -2); + + static final Point1D TEST_POINT_1D_1 = new Point1D(2.2); + static final Point1D TEST_POINT_1D_2 = new Point1D(-2.2); + static final Point1D TEST_POINT_1D_3 = new Point1D(-3.3); + static final Point1D TEST_POINT_1D_4 = new Point1D(5.5); + static final Point1D TEST_POINT_1D_MISSING = new Point1D(-1.0); // Nearest to TEST_POINT_1D_2 + static final Point1D TEST_POINT_1D_INVALID = new Point1D(-1.0, true); + + static final String TEST_OBJECT_1 = "TEST 1"; + static final String TEST_OBJECT_2 = "TEST 2"; + static final String TEST_OBJECT_3 = "TEST 3"; + + + /***************** + * TREE CREATION * + *****************/ + + @Test + public void testKDTree_creationSuccess() { + TypedKDTree treeA = new TypedKDTree(); + Assert.assertNotNull(treeA); + + TypedKDTree treeB = new TypedKDTree(); + treeB.initialize(5); + Assert.assertNotNull(treeB); + Assert.assertEquals(treeB.dimensions(), 5); + } + + @Test + public void testKDTree_canInitialize() { + TypedKDTree treeA = new TypedKDTree(); + treeA.initialize(3); + Assert.assertEquals(treeA.dimensions(), 3); + } + + @Test(expected = IllegalArgumentException.class) + public void testKDTree_creationFailureZero() { + TypedKDTree treeA = new TypedKDTree(); + treeA.initialize(0); + } + + @Test(expected = UnsupportedOperationException.class) + public void testKDTree_creationFailureRepeated() { + TypedKDTree treeA = new TypedKDTree(); + treeA.initialize(3); + treeA.initialize(4); + } + + + /****************** + * TREE INSERTION * + ******************/ + + @Test + public void testKDTree_canAdd3DData() { + // Can add an item + TypedKDTree treeA = new TypedKDTree(); + treeA.insert(TEST_POINT_3D_1, TEST_OBJECT_1); + Assert.assertEquals(treeA.size(), 1); + + // Add same point again, should be no change in count + treeA.insert(TEST_POINT_3D_1, TEST_OBJECT_1); + Assert.assertEquals(treeA.size(), 1); + + // Add more points + treeA.insert(TEST_POINT_3D_2, TEST_OBJECT_2); + treeA.insert(TEST_POINT_3D_3, TEST_OBJECT_3); + treeA.insert(TEST_POINT_3D_4, TEST_OBJECT_3); // With repeat value + Assert.assertEquals(treeA.size(), 4); + } + + @Test + public void testKDTree_canAdd2DData() { + // Can add an item + TypedKDTree treeA = new TypedKDTree(); + treeA.insert(TEST_POINT_2D_1, TEST_OBJECT_1); + treeA.insert(TEST_POINT_2D_2, TEST_OBJECT_2); + Assert.assertEquals(treeA.size(), 2); + + // Add same point again, should be no change in count + treeA.insert(TEST_POINT_2D_1, TEST_OBJECT_3); + Assert.assertEquals(treeA.size(), 2); + } + + @Test + public void testKDTree_canAdd1DData() { + // Can add an item + TypedKDTree treeA = new TypedKDTree(); + + treeA.insert(TEST_POINT_1D_1, TEST_OBJECT_1); + treeA.insert(TEST_POINT_1D_2, TEST_OBJECT_2); + Assert.assertEquals(treeA.size(), 2); + + // Add same point again, should be no change in count + treeA.insert(TEST_POINT_1D_1, TEST_OBJECT_3); + Assert.assertEquals(treeA.size(), 2); + } + + @Test + public void testKDTree_canAddNullData() { + // Can add an item + TypedKDTree treeA = new TypedKDTree(); + treeA.insert(TEST_POINT_1D_1, null); + treeA.insert(TEST_POINT_1D_2, null); + Assert.assertEquals(treeA.size(), 2); + + // Add same point again, should be no change in count + treeA.insert(TEST_POINT_1D_1, null); + Assert.assertEquals(treeA.size(), 2); + } + + @Test + public void testKDTree_canAddMultipleData() { + // Can add an items + TypedKDTree treeA = new TypedKDTree(); + HashMap map = new HashMap(); + map.put(TEST_POINT_3D_1, TEST_OBJECT_1); + map.put(TEST_POINT_3D_2, TEST_OBJECT_2); + map.put(TEST_POINT_3D_3, TEST_OBJECT_3); + map.put(TEST_POINT_3D_4, TEST_OBJECT_3); + + treeA.insert(map); + Assert.assertEquals(treeA.size(), 4); + } + + @Test(expected = IllegalStateException.class) + public void testKDTree_cannotAddMixedKeys() { + // Attempt to add 2D to a 3D tree + TypedKDTree treeA = new TypedKDTree(); + treeA.insert(TEST_POINT_1D_1, TEST_OBJECT_1); + treeA.insert(TEST_POINT_1D_INVALID, TEST_OBJECT_2); + } + + @Test + public void testKDTree_canAddNullKey() { + // Attempt to add null to a 3D tree + TypedKDTree treeA = new TypedKDTree(); + treeA.insert(null, TEST_OBJECT_1); + Assert.assertEquals(treeA.size(), 0); + } + + + /****************** + * TREE SEARCHING * + ******************/ + + @Test + public void testKDTree_canSearchByPresentKey3D() { + TypedKDTree treeA = createTreeWithTestData3D(); + + // Get by valid key + Object result = treeA.search(TEST_POINT_3D_2); + Assert.assertNotNull(result); + Assert.assertEquals(result, TEST_OBJECT_2); + } + + @Test + public void testKDTree_canSearchByMissingKey3D() { + TypedKDTree treeA = createTreeWithTestData3D(); + + // Get by missing valid key + String result = treeA.search(TEST_POINT_3D_MISSING); + Assert.assertNull(result); + } + + @Test + public void testKDTree_canSearchByPresentKey1D() { + TypedKDTree treeA = createTreeWithTestData1D(); + + // Get by valid key + Object result = treeA.search(TEST_POINT_1D_3); + Assert.assertNotNull(result); + Assert.assertEquals(result, TEST_OBJECT_3); + } + + @Test + public void testKDTree_canSearchNullKey() { + TypedKDTree treeA = createTreeWithTestData3D(); + + // Search by invalid key (null) + String result = treeA.search(null); + Assert.assertNull(result); + } + + + /***************** + * TREE DELETING * + *****************/ + + @Test + public void testKDTree_canDeletePresentKey3D() { + TypedKDTree treeA = createTreeWithTestData3D(); + + // Delete data, check it's gone + treeA.delete(TEST_POINT_3D_2); + String result = treeA.search(TEST_POINT_3D_2); + Assert.assertNull(result); + } + + @Test + public void testKDTree_canDeletePresentKey1D() { + TypedKDTree treeA = createTreeWithTestData1D(); + + // Delete data, check it's gone + treeA.delete(TEST_POINT_1D_2); + String result = treeA.search(TEST_POINT_1D_2); + Assert.assertNull(result); + } + + @Test + public void testKDTree_canDeleteMultipleKeys3D() { + TypedKDTree treeA = createTreeWithTestData3D(); + ArrayList keys = new ArrayList(); + keys.add(TEST_POINT_3D_1); + keys.add(TEST_POINT_3D_2); + + // Delete data, check it's gone + treeA.delete(keys); + String result1 = treeA.search(TEST_POINT_3D_1); + String result2 = treeA.search(TEST_POINT_3D_2); + Assert.assertNull(result1); + Assert.assertNull(result2); + } + + @Test(expected = IllegalStateException.class) + public void testKDTree_cannotDeleteInvalidKey() { + TypedKDTree treeA = createTreeWithTestData1D(); + + // Search by invalid key (wrong size) + treeA.delete(TEST_POINT_1D_INVALID); + } + + @Test + public void testKDTree_canDeleteNullKey() { + TypedKDTree treeA = createTreeWithTestData3D(); + Assert.assertEquals(treeA.size(), 4); + + // Search by invalid key (null) + treeA.delete((Point3D) null); + + Assert.assertEquals(treeA.size(), 4); + } + + @Test + public void testKDTree_canDeleteByMissingKey() { + TypedKDTree treeA = createTreeWithTestData3D(); + Assert.assertEquals(treeA.size(), 4); + + // Delete by missing key + treeA.delete(TEST_POINT_3D_MISSING); + + Assert.assertEquals(treeA.size(), 4); + } + + + /***************************** + * TREE NEAREST - SINGLE ARG * + *****************************/ + + @Test + public void testKDTree_canGetNearestWithPresentKey3D() { + TypedKDTree treeA = createTreeWithTestData3D(); + + String result = treeA.nearest(TEST_POINT_3D_MISSING); + Assert.assertNotNull(result); + Assert.assertEquals(result, TEST_OBJECT_2); // from TEST_POINT_3D_2 + } + + @Test + public void testKDTree_canGetNearestWithPresentKey1D() { + TypedKDTree treeA = createTreeWithTestData1D(); + + Object result = treeA.nearest(TEST_POINT_1D_MISSING); + Assert.assertNotNull(result); + Assert.assertEquals(result, TEST_OBJECT_2); // from TEST_POINT_1D_2 + } + + @Test(expected = IllegalStateException.class) + public void testKDTree_cannotGetNearestWithInvalidKey() { + TypedKDTree treeA = createTreeWithTestData1D(); + + // Search by invalid key (wrong size) + treeA.nearest(TEST_POINT_1D_INVALID); + } + + @Test + public void testKDTree_canGetNearestWithNullKey() { + TypedKDTree treeA = createTreeWithTestData3D(); + + // Search by invalid key (null) + String result = treeA.nearest(null); + Assert.assertNull(result); + } + + @Test + public void testKDTree_canGetNearestWithLackOfData() { + TypedKDTree treeA = new TypedKDTree(); + String result = treeA.nearest(TEST_POINT_3D_1); + Assert.assertNull(result); + } + + + /******************************* + * TREE NEAREST - MULTIPLE ARG * + *******************************/ + + @Test + public void testKDTree_canGetNearest2WithPresentKey3D() { + TypedKDTree treeA = createTreeWithTestData3D(); + + List result = treeA.nearest(TEST_POINT_3D_MISSING, 2); + Assert.assertNotNull(result); + Assert.assertNotNull(result.get(0)); + Assert.assertNotNull(result.get(1)); + Assert.assertEquals(result.get(0), TEST_OBJECT_2); // from TEST_POINT_3D_2 + Assert.assertEquals(result.get(1), TEST_OBJECT_1); // from TEST_POINT_3D_1 + } + + @Test + public void testKDTree_canGetNearest2WithPresentKey1D() { + TypedKDTree treeA = createTreeWithTestData1D(); + + List result = treeA.nearest(TEST_POINT_1D_MISSING, 3); + Assert.assertNotNull(result); + Assert.assertNotNull(result.get(0)); + Assert.assertNotNull(result.get(1)); + Assert.assertNotNull(result.get(2)); + Assert.assertEquals(result.get(0), TEST_OBJECT_2); // from TEST_POINT_1D_2 + Assert.assertEquals(result.get(1), TEST_OBJECT_3); // from TEST_POINT_1D_3 + Assert.assertEquals(result.get(2), TEST_OBJECT_1); // from TEST_POINT_1D_1 + } + + @Test(expected = IllegalStateException.class) + public void testKDTree_cannotGetNearest2WithInvalidKey() { + TypedKDTree treeA = createTreeWithTestData1D(); + + // Search by invalid key (wrong size) + treeA.nearest(TEST_POINT_1D_INVALID, 2); + } + + @Test + public void testKDTree_canGetNearest2WithNullKey() { + TypedKDTree treeA = createTreeWithTestData3D(); + + // Search by invalid key (null) + List result = treeA.nearest(null, 2); + Assert.assertNotNull(result); + Assert.assertEquals(result.size(), 0); + } + + @Test + public void testKDTree_canGetNearest2WithLackOfData() { + TypedKDTree treeA = new TypedKDTree(); + List result = treeA.nearest(TEST_POINT_3D_1, 2); + Assert.assertNotNull(result); + Assert.assertEquals(result.size(), 0); + } + + @Test + public void testKDTree_canGetNearestWithInvalidCount() { + TypedKDTree treeA = createTreeWithTestData3D(); + + // Search by missing key + List result = treeA.nearest(TEST_POINT_3D_1, 1000); + Assert.assertNotNull(result); + Assert.assertEquals(result.size(), 4); + } + + + /********************* + * TREE NEAREST KEYS * + *********************/ + + @Test + public void testKDTree_canGetNearestAsKeys() { + TypedKDTree treeA = createTreeWithTestData3D(); + + List results = treeA.nearestKeys(TEST_POINT_3D_1, 1); + Assert.assertNotNull(results); + Assert.assertTrue(results.size() > 0); + Assert.assertArrayEquals( + results.get(0).getKDTreePoint(), + TEST_POINT_3D_1.getKDTreePoint(), + 0.0 + ); + } + + @Test(expected = IllegalStateException.class) + public void testKDTree_cannotGetNearestKeysWithInvalidKey() { + TypedKDTree treeA = createTreeWithTestData1D(); + + // Search by invalid key (wrong size) + treeA.nearestKeys(TEST_POINT_1D_INVALID, 1); + } + + @Test + public void testKDTree_canGetNearestKeysWithNullKey() { + TypedKDTree treeA = createTreeWithTestData3D(); + + // Search by invalid key (null) + List results = treeA.nearestKeys(null, 1); + Assert.assertEquals(results.size(), 0); + } + + @Test + public void testKDTree_canGetNearestKeysWithLackOfData() { + TypedKDTree treeA = new TypedKDTree(); + + // Search by missing key + List results = treeA.nearestKeys(TEST_POINT_3D_1, 1); + Assert.assertEquals(results.size(), 0); + } + + @Test + public void testKDTree_canGetNearestKeysWithInvalidCount() { + TypedKDTree treeA = createTreeWithTestData3D(); + + // Search by missing key + List result = + treeA.nearestKeys(TEST_POINT_3D_1, 1000); + + Assert.assertNotNull(result); + Assert.assertEquals(result.size(), 4); + } + + + /************** + * TREE RANGE * + **************/ + + @Test + public void testKDTree_canRangeSearch3D() { + TypedKDTree treeA = createTreeWithTestData3D(); + + List result = treeA.range(TEST_RANGE_3D_1, TEST_RANGE_3D_2); + Assert.assertNotNull(result); + Assert.assertEquals(result.size(), 2); + Assert.assertNotNull(result.get(0)); + Assert.assertNotNull(result.get(1)); + Assert.assertEquals(result.get(0), TEST_OBJECT_1); // from TEST_POINT_3D_1 + Assert.assertEquals(result.get(1), TEST_OBJECT_3); // from TEST_POINT_3D_4 + } + + @Test + public void testKDTree_canRangeSearch1D() { + TypedKDTree treeA = createTreeWithTestData1D(); + + List result = treeA.range(TEST_POINT_1D_2, TEST_POINT_1D_4); + Assert.assertEquals(result.size(), 3); + } + + @Test(expected = IllegalStateException.class) + public void testKDTree_cannotRangeSearchWithInvalidKeyDiff() { + TypedKDTree treeA = createTreeWithTestData1D(); + + // Search by invalid key (wrong size) + treeA.range(TEST_POINT_1D_INVALID, TEST_POINT_1D_MISSING); + } + + @Test(expected = IllegalStateException.class) + public void testKDTree_cannotRangeSearchWithInvalidKeyWrong() { + TypedKDTree treeA = createTreeWithTestData1D(); + + // Search by invalid key (wrong size) + treeA.range(TEST_POINT_1D_INVALID, TEST_POINT_1D_INVALID); + } + + @Test + public void testKDTree_canRangeSearchWithInvalidKeyNullLeft() { + TypedKDTree treeA = createTreeWithTestData3D(); + + // Search by invalid key (null) + List result = treeA.range(null, TEST_RANGE_3D_2); + Assert.assertNotNull(result); + Assert.assertEquals(result.size(), 0); + } + + @Test + public void testKDTree_canRangeSearchWithInvalidKeyNullRight() { + TypedKDTree treeA = createTreeWithTestData3D(); + + // Search by invalid key (null) + List result = treeA.range(TEST_RANGE_3D_2, null); + Assert.assertNotNull(result); + Assert.assertEquals(result.size(), 0); + } + + @Test + public void testKDTree_canRangeSearchWithLackOfData() { + TypedKDTree treeA = new TypedKDTree(); + List result = treeA.range(TEST_RANGE_3D_1, TEST_RANGE_3D_2); + Assert.assertNotNull(result); + Assert.assertEquals(result.size(), 0); + } + + /********************* + * TREE OPTIMIZATION * + *********************/ + + @Test + public void testKDTree_canOptimizeTree() { + TypedKDTree treeA = new TypedKDTree(); + + // Initial tree items inserted in a random, suboptimal order + treeA.insert(new Point3D(10, 20, 30), "A") + .insert(new Point3D(-1, -2, -3), "B") + .insert(new Point3D(1, 1, 1), "C") + .insert(new Point3D(10, 2, 3), "D") + .insert(new Point3D(1, 29, 3), "E") // Specified center point + .insert(new Point3D(-10, 2, -3), "F") + .insert(new Point3D(0, 0, 0), "G") + .insert(new Point3D(2, 6, 3), "H") + .delete(new Point3D(0, 0, 0)); + + String snapshot1 = treeA.toString(); + List snapshotA = new ArrayList(treeA.toMap().values()); + Assert.assertEquals(snapshotA.get(0), "A"); + + // Optimize around the median of all points for each dimension + treeA.optimize(new Point3D(1.5, 29.5, 3.5)); // near the "E" point + + String snapshot2 = treeA.toString(); + List snapshotB = new ArrayList(treeA.toMap().values()); + Assert.assertEquals(snapshotB.get(0), "E"); + + // The .toString method shows deleted notes with a '*'. So the second + // snapshot should have less characters than the first. + Assert.assertTrue(snapshot1.length() > snapshot2.length()); + } + + /************** + * TREE UTILS * + **************/ + + @Test + public void testKDTree_canGetLastValue() { + TypedKDTree treeA = createTreeWithTestData3D(); + String lastValue = treeA.getLastValue(); + Assert.assertEquals(lastValue, TEST_OBJECT_3); + + lastValue = treeA.delete(TEST_POINT_3D_2).getLastValue(); + Assert.assertEquals(lastValue, TEST_OBJECT_2); + } + + @Test + public void testKDTree_canGetLastValues() { + TypedKDTree treeA = new TypedKDTree(); + HashMap map = new HashMap(); + map.put(TEST_POINT_3D_1, TEST_OBJECT_1); + map.put(TEST_POINT_3D_2, TEST_OBJECT_2); + map.put(TEST_POINT_3D_3, TEST_OBJECT_3); + map.put(TEST_POINT_3D_4, TEST_OBJECT_3); + + Map lastValues = treeA.insert(map).getLastValues(); + Assert.assertEquals(lastValues.size(), 4); + Assert.assertEquals(lastValues.get(TEST_POINT_3D_2), TEST_OBJECT_2); + + List keyList = new ArrayList(map.keySet()); + lastValues = treeA.delete(keyList).getLastValues(); + Assert.assertEquals(lastValues.size(), 4); + Assert.assertEquals(lastValues.get(TEST_POINT_3D_2), TEST_OBJECT_2); + } + + @Test + public void testKDTree_canGetTreeSize() { + TypedKDTree treeA = createTreeWithTestData3D(); + Assert.assertEquals(treeA.size(), 4); + + TypedKDTree treeB = new TypedKDTree(); + Assert.assertEquals(treeB.size(), 0); + } + + @Test + public void testKDTree_canGetTreeDimensions() { + TypedKDTree treeA = createTreeWithTestData3D(); + Assert.assertEquals(treeA.dimensions(), 3); + + TypedKDTree treeB = createTreeWithTestData1D(); + Assert.assertEquals(treeB.dimensions(), 1); + } + + @Test + public void testKDTree_canGetTreeAsString() { + TypedKDTree treeA = createTreeWithTestData3D(); + Assert.assertTrue(treeA.toString().length() > 4); + + TypedKDTree treeB = new TypedKDTree(); + Assert.assertEquals(treeB.toString(), "null"); + } + + @Test + public void testKDTree_canGetTreeAsMap() { + TypedKDTree treeA = createTreeWithTestData3D(); + Map testTreeMap = treeA.toMap(); + Assert.assertEquals(testTreeMap.size(), 4); + + treeA.delete(TEST_POINT_3D_3); + testTreeMap = treeA.toMap(); + Assert.assertEquals(testTreeMap.size(), 3); + } + + /*************** + * COMBO TESTS * + ***************/ + + @Test + public void testKDTree_canUseChaining() { + TypedKDTree treeA = new TypedKDTree(); + String lastValue = + treeA.insert(TEST_POINT_3D_1, TEST_OBJECT_1) + .insert(TEST_POINT_3D_2, TEST_OBJECT_2) + .insert(TEST_POINT_3D_3, TEST_OBJECT_3) + .insert(TEST_POINT_3D_4, TEST_OBJECT_3) + .delete(TEST_POINT_3D_3) + .getLastValue(); + + Assert.assertEquals(treeA.size(), 3); + Assert.assertEquals(lastValue, TEST_OBJECT_3); + } + + @Test + public void testKDTree_canUpdateData() { + // Can add an item + TypedKDTree treeA = createTreeWithTestData3D(); + Assert.assertEquals(treeA.search(TEST_POINT_3D_1), TEST_OBJECT_1); + Assert.assertEquals(treeA.size(), 4); + + treeA.insert(TEST_POINT_3D_1, TEST_OBJECT_3); + Assert.assertEquals(treeA.search(TEST_POINT_3D_1), TEST_OBJECT_3); + Assert.assertEquals(treeA.size(), 4); + } + + @Test + public void testKDTree_sizeIsCorrect() { + TypedKDTree treeA = createTreeWithTestData3D(); + Assert.assertEquals(treeA.size(), 4); + + // Delete data, check size + treeA.delete(TEST_POINT_3D_1); + Assert.assertEquals(treeA.size(), 3); + + treeA.insert(TEST_POINT_3D_1, TEST_OBJECT_1); + Assert.assertEquals(treeA.size(), 4); + + treeA.insert(TEST_POINT_3D_MISSING, TEST_OBJECT_2); + Assert.assertEquals(treeA.size(), 5); + } + + @Test + public void testKDTree_canSearchAfterEmptying() { + TypedKDTree treeA = createTreeWithTestData3D(); + + // Delete all points + treeA.delete(TEST_POINT_3D_1); + treeA.delete(TEST_POINT_3D_2); + treeA.delete(TEST_POINT_3D_3); + treeA.delete(TEST_POINT_3D_4); + + // Search while data is supposedly empty + String result = treeA.nearest(TEST_POINT_3D_1); + Assert.assertNull(result); + } + + + /** Private test helpers */ + private TypedKDTree createTreeWithTestData3D() { + TypedKDTree treeA = new TypedKDTree(); + treeA.insert(TEST_POINT_3D_1, TEST_OBJECT_2); + treeA.insert(TEST_POINT_3D_1, TEST_OBJECT_1); // With repeat key, updated value + treeA.insert(TEST_POINT_3D_2, TEST_OBJECT_2); + treeA.insert(TEST_POINT_3D_3, TEST_OBJECT_3); + treeA.insert(TEST_POINT_3D_4, TEST_OBJECT_3); // With repeat value + + return treeA; + } + + private TypedKDTree createTreeWithTestData1D() { + TypedKDTree treeA = new TypedKDTree(); + treeA.insert(TEST_POINT_1D_1, TEST_OBJECT_1); + treeA.insert(TEST_POINT_1D_2, TEST_OBJECT_2); + treeA.insert(TEST_POINT_1D_3, TEST_OBJECT_3); + treeA.insert(TEST_POINT_1D_4, TEST_OBJECT_3); // With repeat value + + return treeA; + } +} diff --git a/src/net/sf/javaml/core/kdtree/KDNode.java b/src/net/sf/javaml/core/kdtree/KDNode.java index 5dcda36..c5f2eb2 100644 --- a/src/net/sf/javaml/core/kdtree/KDNode.java +++ b/src/net/sf/javaml/core/kdtree/KDNode.java @@ -7,6 +7,8 @@ package net.sf.javaml.core.kdtree; import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Vector; // K-D Tree node class @@ -23,29 +25,23 @@ class KDNode implements Serializable { protected boolean deleted; // Method ins translated from 352.ins.c of Gonnet & Baeza-Yates - protected static KDNode ins(HPoint key, Object val, KDNode t, int lev, int K) { + protected static KDNode ins(HPoint key, Object val, KDNode t, int lev, int K, boolean[] isUpdate) { if (t == null) { t = new KDNode(key, val); } else if (key.equals(t.k)) { - - // "re-insert" - if (t.deleted) { - t.deleted = false; - t.v = val; - } - - // else { - // throw new KeyDuplicateException(); - // } + // "re-insert" or update + isUpdate[0] = !t.deleted; + t.deleted = false; + t.v = val; } else if (key.coord[lev] > t.k.coord[lev]) { - t.right = ins(key, val, t.right, (lev + 1) % K, K); + t.right = ins(key, val, t.right, (lev + 1) % K, K, isUpdate); } else { - t.left = ins(key, val, t.left, (lev + 1) % K, K); + t.left = ins(key, val, t.left, (lev + 1) % K, K, isUpdate); } return t; @@ -210,6 +206,19 @@ else if (pivot_to_target < max_dist_sqd) { } } + // Get all data points in a map + protected Map recursiveMap() { + Map map = new LinkedHashMap(); + if (!deleted) { + map.put(k.coord, v); + } + + if (left != null) map.putAll(left.recursiveMap()); + if (right != null) map.putAll(right.recursiveMap()); + + return map; + } + // constructor is used only by class; other methods are static private KDNode(HPoint key, Object val) { diff --git a/src/net/sf/javaml/core/kdtree/KDTree.java b/src/net/sf/javaml/core/kdtree/KDTree.java index 0e5b627..48d230e 100644 --- a/src/net/sf/javaml/core/kdtree/KDTree.java +++ b/src/net/sf/javaml/core/kdtree/KDTree.java @@ -7,7 +7,9 @@ package net.sf.javaml.core.kdtree; import java.io.Serializable; +import java.util.Map; import java.util.Vector; +import java.util.NoSuchElementException; /** * KDTree is a class supporting KD-tree insertion, deletion, equality search, @@ -29,10 +31,10 @@ * @version %I%, %G% * @since JDK1.2 */ -public class KDTree implements Serializable{ +public class KDTree implements Serializable { // K = number of dimensions - private int m_K; + private final int m_K; // root of KD-tree private KDNode m_root; @@ -45,9 +47,16 @@ public class KDTree implements Serializable{ * * @param k * number of dimensions + * + * @throws IllegalArgumentException + * if the key size provided is less than 1 */ public KDTree(int k) { + if (k < 1) { + throw new IllegalArgumentException("KDTree: invalid key size!"); + } + m_K = k; m_root = null; } @@ -69,21 +78,23 @@ public KDTree(int k) { * @param value * value at that key * - * @throws KeySizeException + * @throws IllegalArgumentException * if key.length mismatches K - * @throws KeyDuplicateException - * if key already in tree */ public void insert(double[] key, Object value) { + boolean[] isUpdate = { false }; - if (key.length != m_K) { - throw new RuntimeException("KDTree: wrong key size!"); + if (key == null || key.length != m_K) { + throw new IllegalArgumentException("KDTree: wrong key size!"); } - else - m_root = KDNode.ins(new HPoint(key), value, m_root, 0, m_K); + else { + m_root = KDNode.ins(new HPoint(key), value, m_root, 0, m_K, isUpdate); - m_count++; + if (!isUpdate[0]) { + m_count++; + } + } } /** @@ -95,13 +106,13 @@ public void insert(double[] key, Object value) { * * @return object at key, or null if not found * - * @throws KeySizeException + * @throws IllegalArgumentException * if key.length mismatches K */ public Object search(double[] key) { - if (key.length != m_K) { - throw new RuntimeException("KDTree: wrong key size!"); + if (key == null || key.length != m_K) { + throw new IllegalArgumentException("KDTree: wrong key size!"); } KDNode kd = KDNode.srch(new HPoint(key), m_root, m_K); @@ -117,22 +128,22 @@ public Object search(double[] key) { * @param key * key for KD-tree node * - * @throws KeySizeException + * @throws IllegalArgumentException * if key.length mismatches K - * @throws KeyMissingException + * @throws NoSuchElementException * if no node in tree has key */ public void delete(double[] key) { - if (key.length != m_K) { - throw new RuntimeException("KDTree: wrong key size!"); + if (key == null || key.length != m_K) { + throw new IllegalArgumentException("KDTree: wrong key size!"); } else { KDNode t = KDNode.srch(new HPoint(key), m_root, m_K); if (t == null) { - throw new RuntimeException("KDTree: key missing!"); + throw new NoSuchElementException("KDTree: key missing!"); } else { t.deleted = true; } @@ -162,8 +173,10 @@ public void delete(double[] key) { * * @return object at node nearest to key, or null on failure * - * @throws KeySizeException + * @throws IllegalArgumentException * if key.length mismatches K + * @throws IndexOutOfBoundsException + * if n is negative or exceeds tree size */ public Object nearest(double[] key) { @@ -171,6 +184,56 @@ public Object nearest(double[] key) { return nbrs[0]; } + /** + * Find KD-tree nodes whose keys are n nearest neighbors to key. Uses + * algorithm above. Neighbors are returned in ascending order of distance to + * key. + * + * @param key + * key for KD-tree node + * @param n + * how many neighbors to find + * + * @return objects at node nearest to key, or null on failure + * + * @throws IllegalArgumentException + * if key.length mismatches K + * @throws IndexOutOfBoundsException + * if n is negative or exceeds tree size + */ + public Object[] nearest(double[] key, int n) { + return nearest(key, n, false); + } + + /** + * Find KD-tree node keys whose keys are n nearest neighbors to key. + * Uses algorithm above. Neighbors are returned in ascending order of + * distance to key. + * + * @param key + * key for KD-tree node + * @param n + * how many neighbors to find + * + * @return objects at node nearest to key, or null on failure + * + * @throws IllegalArgumentException + * if key.length mismatches K + * @throws IndexOutOfBoundsException + * if n is negative or exceeds tree size + */ + public double[][] nearestKeys(double[] key, int n) { + Object[] results = nearest(key, n, true); + + double[][] keys = new double[results.length][]; + + for (int i = 0; i < results.length; i++) { + keys[i] = (double[]) results[i]; + } + + return keys; + } + /** * Find KD-tree nodes whose keys are n nearest neighbors to key. Uses * algorithm above. Neighbors are returned in ascending order of distance to @@ -180,23 +243,25 @@ public Object nearest(double[] key) { * key for KD-tree node * @param n * how many neighbors to find + * @param isReturnKey + * true to return the nearest key rather than value * * @return objects at node nearest to key, or null on failure * - * @throws KeySizeException - * if key.length mismatches K * @throws IllegalArgumentException + * if key.length mismatches K + * @throws IndexOutOfBoundsException * if n is negative or exceeds tree size */ - public Object[] nearest(double[] key, int n) { + private Object[] nearest(double[] key, int n, boolean isReturnKey) { - if (n < 0 || n > m_count) { - throw new IllegalArgumentException("Number of neighbors ("+n+") cannot" - + " be negative or greater than number of nodes ("+m_count+")."); + if (key == null || key.length != m_K) { + throw new IllegalArgumentException("KDTree: wrong key size!"); } - if (key.length != m_K) { - throw new RuntimeException("KDTree: wrong key size!"); + if (n < 0 || n > m_count) { + throw new IndexOutOfBoundsException("Number of neighbors (" + n + ") cannot" + + " be negative or greater than number of nodes (" + m_count + ")."); } Object[] nbrs = new Object[n]; @@ -211,7 +276,7 @@ public Object[] nearest(double[] key, int n) { for (int i = 0; i < n; ++i) { KDNode kd = (KDNode) nnl.removeHighest(); - nbrs[n - i - 1] = kd.v; + nbrs[n - i - 1] = isReturnKey ? kd.k.coord : kd.v; } return nbrs; @@ -228,32 +293,54 @@ public Object[] nearest(double[] key, int n) { * * @return array of Objects whose keys fall in range [lowk,uppk] * - * @throws KeySizeException + * @throws IllegalArgumentException * on mismatch among lowk.length, uppk.length, or K */ public Object[] range(double[] lowk, double[] uppk) { - if (lowk.length != uppk.length) { - throw new RuntimeException("KDTree: wrong key size!"); + if (lowk == null || uppk == null || lowk.length != uppk.length || lowk.length != m_K) { + throw new IllegalArgumentException("KDTree: wrong key size!"); } - else if (lowk.length != m_K) { - throw new RuntimeException("KDTree: wrong key size!"); + Vector v = new Vector(); + KDNode.rsearch(new HPoint(lowk), new HPoint(uppk), m_root, 0, m_K, v); + Object[] o = new Object[v.size()]; + for (int i = 0; i < v.size(); ++i) { + KDNode n = v.elementAt(i); + o[i] = n.v; } + return o; + } - else { - Vector v = new Vector(); - KDNode.rsearch(new HPoint(lowk), new HPoint(uppk), m_root, 0, m_K, v); - Object[] o = new Object[v.size()]; - for (int i = 0; i < v.size(); ++i) { - KDNode n = v.elementAt(i); - o[i] = n.v; - } - return o; - } + /** + * Get all the data points of the KDTree. + * + * @return returns a map of all the data points + */ + public Map toMap() { + if (m_root == null) return null; + return m_root.recursiveMap(); + } + + /** + * The size of the KDTree. + * + * @return returns the number of key-value mappings in this KDTree. + */ + public int size() { + return m_count; + } + + /** + * The number of dimensions of the KDTree. + * + * @return returns the number of dimensions of this KDTree. + */ + public int dimensions() { + return m_K; } public String toString() { - return m_root.toString(0); + return m_root != null ? m_root.toString(0) : "null"; } } diff --git a/src/net/sf/javaml/core/kdtree/TypedKDTree.java b/src/net/sf/javaml/core/kdtree/TypedKDTree.java new file mode 100644 index 0000000..cdf29db --- /dev/null +++ b/src/net/sf/javaml/core/kdtree/TypedKDTree.java @@ -0,0 +1,551 @@ +/** + * %SVN.HEADER% + *

+ * based on work by Simon Levy + * http://www.cs.wlu.edu/~levy/software/kd/ + */ +package net.sf.javaml.core.kdtree; + +import java.io.Serializable; +import java.util.*; + +/** + * TypedKDTree is a wrapper class for KDTree that adds in typing. To construct + * this tree the developer will need to provide generics for both the key and + * the value types. It has several differences to KDTree: + * - + * • The underlying KDTree is lazily created once the first point is inserted, + * who's dimensional size will be used as the source of truth from thereon + * - + * • Generics restrict the key and value types to avoid runtime errors. The + * key type must be a class implementing interface `TypedKDTree.Point`. + * This means you won't need to cast return values. See the below example. + * - + * • `insert` and `delete` methods return a reference to the typed tree itself + * which can be used for method chaining! Best combined with + * `getLastValue` and `getLastValues` (only works on the last action). + * - + * • There are 8 new or expanded methods, from bulk insertion/deletion to + * a method that can optimize the tree layout magically. + * - + * • Every method returns a value: + * 1. Either the tree itself, a value, or void if operational. + * 2. If returning a value, then null if the value is not found. + * 3. If the key provided is null (or provides null), the return is null. + * - + * • `nearest(K key, int n)` and `nearestKeys(K key, int n)` will + * automatically round "n" down if greater than the size of the tree. This + * prevents an exception that KDTree would otherwise throw. + * - + * • `toMap` will return keys as generic `Point` objects, but these will not + * be the same as your key type, you may want to convert them. + * - + * • Most methods now only throw IllegalStateException, when your `Point` + * implementation has not been done correctly. + * + * @example + * // 1. Have your existing class implement TypedKDTree.KDTreePoint OR + * // create a small new class like the one below: + * class Point3D implements TypedKDTree.Point { + * private final double[] point; + * public Point3D(double x, double y, double z) { + * point = new double[]{ x, y, z }; + * } + * @Override + * public double[] getKDTreePoint() { return point; } + * } + * // 2. Create a TypedKDTree with type generics; + * TypedKDTree tree = new TypedKDTree(); + * tree.insert(new Point3D(1, 2, 3), "Yes!") + * .insert(new Point3D(-1, -2, -3), "No!"); // Chaining supported + * String maybe = tree.insert(new Point3D(0, 0, 0), "Maybe").getLastValue(); + * + * @author Paul Dilley + * @version %I%, %G% + * @since JDK1.6 + */ +public class TypedKDTree implements Serializable { + /** + * TypedKDTree only accepts keys that are classes having implemented this + * Point. You can modify your existing class to implement this + * interface, and adding a `getKDTreePoint` method which returns the correct + * coordinate point as a double array, or create your own small helper class + * to do so (see the example in the docs prior). + * At runtime, the `getKDTreePoint` is called to get the double[] point. + */ + public interface Point { + /** + * Return the coordinate as a double array + * @return double array representing each dimension + */ + double[] getKDTreePoint(); + } + + /** + * The underlying KDTree is lazily loaded + */ + private KDTree kdTree = null; + + /** + * Caches the last inserted or deleted values + */ + private Map lastValues = new LinkedHashMap(); + private K lastKey = null; + private boolean isMultipleValues = false; + + /** + * The underlying KDTree is lazily created the first time `insert` is called + * However you may want to pre-emptively initialize it if you are worried + * about performance or memory allocation later down the line, or you don't + * control the `Point` implementation and want to enforce a certain + * number of dimensions. + * + * @param dimensions + * number of dimensions + * + * @throws IllegalArgumentException if the dimensions provided is < 1 + * @throws UnsupportedOperationException if already initialized + */ + public void initialize(int dimensions) { + if (dimensions < 1) { + throw new IllegalArgumentException("TypedKDTree: Your " + + "`Point` must have dimensions >= 1"); + } + + if (kdTree == null) { + kdTree = new KDTree(dimensions); + } else { + throw new UnsupportedOperationException("TypedKDTree: The " + + "underlying KDTree has already been initialized"); + } + } + + /** + * Insert a node in a KD-tree. Uses algorithm translated from 352.ins.c of + * + * @param key + * key for TypedKDTree node + * @param value + * value at that key + * + * @return the tree itself, for chaining + * + * @throws IllegalStateException if key length mismatches the size defined + * in the TypedKDTree + */ + public TypedKDTree insert(K key, V value) { + if (!isMultipleValues) { + lastValues = new LinkedHashMap(); + } + + if (isKeyOrTreeNull(key, true)) { + return this; + } + + if (kdTree == null) { + initialize(key.getKDTreePoint().length); + } + + kdTree.insert(key.getKDTreePoint(), value); + lastValues.put(key, value); + lastKey = key; + + return this; + } + + /** + * Insert multiple nodes in a KD-tree + * + * @param keyValuePairs + * a map of the key-value pairs to insert + * + * @return the tree itself, for chaining + * + * @throws IllegalStateException if key length mismatches the size defined + * in the TypedKDTree + */ + public TypedKDTree insert(Map keyValuePairs) { + isMultipleValues = true; + + try { + lastValues = new LinkedHashMap(); + + if (keyValuePairs != null) { + for (Map.Entry entry : keyValuePairs.entrySet()) { + insert(entry.getKey(), entry.getValue()); + } + } + } finally { + isMultipleValues = false; + } + + return this; + } + + /** + * Find KD-tree node whose key is identical to key. Uses algorithm + * translated from 352.srch.c of Gonnet & Baeza-Yates. + * + * @param key + * key for TypedKDTree node + * + * @return typed object at key, or null if not found or key is null + * + * @throws IllegalStateException if key length mismatches the size defined + * in the TypedKDTree + */ + public V search(K key) { + if (isKeyOrTreeNull(key)) return null; + + @SuppressWarnings("unchecked") V result = (V) kdTree.search( + key.getKDTreePoint() + ); + + return result; + } + + /** + * Delete a node from a KD-tree. Instead of actually deleting node and + * rebuilding tree, marks node as deleted. Hence, it is up to the caller to + * call optimize() to prune these deleted nodes + * + * @param key + * key for TypedKDTree node + * + * @return the tree itself, for chaining + * + * @throws IllegalStateException if key length mismatches the size defined + * in the TypedKDTree + */ + public TypedKDTree delete(K key) { + if (!isMultipleValues) { + lastValues = new LinkedHashMap(); + } + + if (isKeyOrTreeNull(key)) return this; + + V result = search(key); + if (result != null) { + kdTree.delete(key.getKDTreePoint()); + lastValues.put(key, result); + lastKey = key; + } + + return this; + } + + /** + * Delete multiple nodes from a KD-tree. + * + * @param keys + * keys for TypedKDTree node + * + * @return the tree itself, for chaining + * + * @throws IllegalStateException if key length mismatches the size defined + * in the TypedKDTree + */ + public TypedKDTree delete(List keys) { + isMultipleValues = true; + + try { + lastValues = new LinkedHashMap(); + + if (keys != null && keys.size() > 0) { + for (K key : keys) { + delete(key); + } + } + } finally { + isMultipleValues = false; + } + + return this; + } + + /** + * Find KD-tree node whose key is nearest neighbor to key. Implements the + * Nearest Neighbor algorithm (Table 6.4) of... + * + * @param key + * key for TypedKDTree node + * + * @return object at node nearest to key, + * or null if not found, tree is empty, or key is invalid + * + * @throws IllegalStateException if key length mismatches the size defined + * in the TypedKDTree + */ + public V nearest(K key) { + if (isKeyOrTreeNull(key) || kdTree.size() == 0) { + return null; + } + + @SuppressWarnings("unchecked") V result = (V) kdTree.nearest( + key.getKDTreePoint() + ); + return result; + } + + /** + * Find KD-tree nodes whose keys are n nearest neighbors to key. Uses + * algorithm above. Neighbors are returned in ascending order of distance to + * key. + * + * @param key + * key for TypedKDTree node + * @param n + * how many neighbors to find + * + * @return objects at node nearest to keys, + * or null if not found, tree is empty, or key is invalid + * (the max items size returned is limited to the current tree size) + * + * @throws IllegalStateException if key length mismatches the size defined + * in the TypedKDTree + */ + public List nearest(K key, int n) { + int size = kdTree != null ? kdTree.size() : 0; + + List results = new ArrayList(); + + // Return empty array if key is null or tree is null/empty + if (isKeyOrTreeNull(key) || size == 0) { + return results; + } + + Object[] v = kdTree.nearest(key.getKDTreePoint(), Math.min(n, size)); + // Return empty array if result is null or no results + if (v == null || v.length == 0) { + return results; + } + + for (Object item : v) { + @SuppressWarnings("unchecked") V typedItem = (V) item; + results.add(typedItem); + } + + return results; + } + + + /** + * Find KD-tree node keys whose keys are n nearest neighbors to key. + * Uses algorithm above. Neighbors are returned in ascending order of + * distance to key. + * Note that the keys will be implementations of `Point`, but will not be + * the Point implementation you provided. + * + * @param key + * key for KD-tree node + * @param n + * how many neighbors to find + * + * @return keys at node nearest to key, or null on failure + * + */ + public List nearestKeys(K key, int n) { + int size = kdTree != null ? kdTree.size() : 0; + + List results = new ArrayList(); + + if (isKeyOrTreeNull(key) || size == 0) { + return results; + } + + double[][] keys = + kdTree.nearestKeys(key.getKDTreePoint(), Math.min(n, size)); + + if (keys == null) return results; + + for (double[] rawKey : keys) { + results.add(new UncheckedPoint(rawKey)); + } + + return results; + } + + /** + * Range search in a KD-tree. Uses algorithm translated from 352.range.c of + * Gonnet & Baeza-Yates. + * + * @param lowk + * lower-bounds for key + * @param uppk + * upper-bounds for key + * + * @return array of Objects whose keys fall in range [lowk,uppk] + * or null if either key is null + * + * @throws IllegalStateException if key length mismatches the size defined + * in the TypedKDTree + */ + public List range(K lowk, K uppk) { + int size = kdTree != null ? kdTree.size() : 0; + List results = new ArrayList(); + + // Return empty array if key is null or tree is null/empty + if (isKeyOrTreeNull(lowk) || isKeyOrTreeNull(uppk) || size == 0) { + return results; + } + + Object[] v = kdTree.range(lowk.getKDTreePoint(), uppk.getKDTreePoint()); + // Return empty array if result is null or no results + if (v == null || v.length == 0) { + return results; + } + + for (Object item : v) { + @SuppressWarnings("unchecked") V typedItem = (V) item; + results.add(typedItem); + } + + return results; + } + + /** + * Get all the data points of the KDTree, with correct value data type. + * The map is ordered: top-down, left-to-right. + * Note that the keys will be implementations of `Point`, but will not be + * the Point implementation you provided. + * + * @return returns a map of all the data points + */ + public Map toMap() { + Map results = + new LinkedHashMap(); + + Map map = kdTree != null ? kdTree.toMap() : null; + + if (map == null) return results; + + Set> mapSet = map.entrySet(); + for (Map.Entry entry : mapSet) { + @SuppressWarnings({"unchecked"}) V value = (V) entry.getValue(); + results.put(new UncheckedPoint(entry.getKey()), value); + } + + return results; + } + + /** + * After using insert or delete, this can be called to get the very last + * value that was inserted or removed from the tree. Useful for chaining. + * + * @return The very last value, either inserted or deleted + */ + public V getLastValue() { + if (lastKey == null || lastValues == null){ + return null; + } + return lastValues.get(lastKey); + } + + /** + * After using insert or delete, this can be called to get the last changes + * made to the tree. This is useful for chaining. + * Note: It only returns values for the final call, so use one call to + * insert multiple values if you need to get them all back. + * + * @return A map of the last changes, either inserted or deleted + */ + public Map getLastValues() { + return lastValues; + } + + /** + * Refreshes the tree, creating a new one that excludes any items marked as + * deleted, and inserts the rest back in the nearest order to the provided + * center point. + * + * @param point + * centre point to determine nearest, and insert into the tree from + * nearest to furthest order + */ + public void optimize(K point) { + if (kdTree == null || kdTree.size() == 0) return; + + if (point == null) { + throw new IllegalArgumentException("TypedKDTree: To optimize the" + + "tree, you must provide a valid center point, but you passed" + + " null"); + } + + int dimensions = kdTree.dimensions(); + int size = kdTree.size(); + double[] centerPoint = point.getKDTreePoint(); + Map map = kdTree.toMap(); + + // Get points in order of nearest from the center point + double[][] result = kdTree.nearestKeys(centerPoint, size); + + // Create a new tree, insert key-values back in, in nearest order + kdTree = new KDTree(dimensions); + for (double[] key : result) { + kdTree.insert(key, map.get(key)); + } + } + + /** + * The size of the KDTree. + * + * @return returns the number of key-value mappings in this KDTree, or 0 if + * not initialized or empty + */ + public int size() { + return kdTree != null ? kdTree.size() : 0; + } + + /** + * The number of dimensions of the KDTree. + * + * @return returns the number of dimensions of this KDTree, or 0 if the + * underlying tree hasn't yet been initialized + */ + public int dimensions() { + return kdTree != null ? kdTree.dimensions() : 0; + } + + /** + * String representation of the tree + * + * @return KDTree to string + */ + public String toString() { + return kdTree != null ? kdTree.toString() : "null"; + } + + + private boolean isKeyOrTreeNull(K key) { + return isKeyOrTreeNull(key, false); + } + private boolean isKeyOrTreeNull(K key, boolean skipTreeCheck) { + if (!skipTreeCheck && kdTree == null) { + return true; + } + + boolean isNullKey = key == null || key.getKDTreePoint() == null; + + // Validate the returned key is the right key length + if (!isNullKey && kdTree != null && + key.getKDTreePoint().length != kdTree.dimensions()) { + throw new IllegalStateException("TypedKDTree: The key returned by" + + " your `KDTreePoint` implementation must have the same length" + + " as the TypedKDTree"); + } + + return isNullKey; + } + + /** + * This is used internally by TypedKDTree. + * It bypasses type checks by just accepting a double[], so it's not for + * use outside of guaranteed situations within this class. + */ + private static class UncheckedPoint implements TypedKDTree.Point { + double[] point; + public UncheckedPoint(double[] point) { this.point = point; } + @Override + public double[] getKDTreePoint() { return point; } + } +}