From 9d2040a8ad831626ee11748cac29885209211e64 Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sun, 9 Jun 2024 16:00:21 +0200 Subject: [PATCH 01/12] Fix for issue #40 --- CHANGELOG.md | 6 +- .../org/tinspin/index/qthypercube2/QNode.java | 89 ++++++++++++++----- .../index/qthypercube2/QuadTreeKD2.java | 20 ++++- .../org/tinspin/index/qt2/Quadtree2Test.java | 41 +++++++++ 4 files changed, 132 insertions(+), 24 deletions(-) create mode 100644 src/test/java/org/tinspin/index/qt2/Quadtree2Test.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 909731e..de30935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Nothing yet +## [2.1.4 - Unreleased] + +- Fixed tree corruption after remove() in QT2. + ## [2.1.3] - 2023-11-19 ### Fixed -- Fixed QUadtreeKD2 kNN finding previously deleted entries [#37](https://github.com/tzaeschke/tinspin-indexes/issue/37) +- Fixed QuadtreeKD2 kNN finding previously deleted entries [#37](https://github.com/tzaeschke/tinspin-indexes/issue/37) ## [2.1.2] - 2023-10-31 diff --git a/src/main/java/org/tinspin/index/qthypercube2/QNode.java b/src/main/java/org/tinspin/index/qthypercube2/QNode.java index 7564b1e..71bf065 100644 --- a/src/main/java/org/tinspin/index/qthypercube2/QNode.java +++ b/src/main/java/org/tinspin/index/qthypercube2/QNode.java @@ -26,7 +26,14 @@ /** * Node class for the quadtree. - * + *

+ * A node can be in one of two modes: "directory node" and "leaf node". + * Directory nodes have an 2^dim array of "subs", where ich entry can be one of: a point, a subnode, or null. + * Leaf nodes have an array of "values" which are all points. + *

+ * A new subnode is inserted in "subs" if a slot in "subs" already contains a point. + * + * * @author ztilmann * * @param Value type. @@ -203,7 +210,9 @@ PointEntry remove(QNode parent, double[] key, int maxNodeSize, Predicate

)o).remove(this, key, maxNodeSize, pred); + PointEntry removed = ((QNode)o).remove(this, key, maxNodeSize, pred); + checkAndMergeLeafNodesInParent(parent, maxNodeSize); + return removed; } else if (o instanceof PointEntry) { PointEntry e = (PointEntry) o; if (removeSub(parent, key, pos, e, maxNodeSize, pred)) { @@ -228,9 +237,7 @@ private boolean removeSub( removeValue(pos); //TODO provide threshold for re-insert // i.e. do not always merge. - if (parent != null) { - parent.checkAndMergeLeafNodes(maxNodeSize); - } + checkAndMergeLeafNodesInParent(parent, maxNodeSize); return true; } return false; @@ -273,9 +280,7 @@ PointEntry update(QNode parent, double[] keyOld, double[] keyNew, int maxN requiresReinsert[0] = false; } else { requiresReinsert[0] = true; - if (parent != null) { - parent.checkAndMergeLeafNodes(maxNodeSize); - } + checkAndMergeLeafNodesInParent(parent, maxNodeSize); } return qe; } @@ -304,9 +309,14 @@ private void updateSub(double[] keyNew, PointEntry e, QNode parent, int ma requiresReinsert[0] = true; //TODO provide threshold for re-insert //i.e. do not always merge. - if (parent != null) { - parent.checkAndMergeLeafNodes(maxNodeSize); - } + checkAndMergeLeafNodesInParent(parent, maxNodeSize); + } + } + + @SuppressWarnings("unchecked") + private void checkAndMergeLeafNodesInParent(QNode parent, int maxNodeSize) { + if (parent != null) { + parent.checkAndMergeLeafNodes(maxNodeSize); } } @@ -314,6 +324,7 @@ private void updateSub(double[] keyNew, PointEntry e, QNode parent, int ma private void checkAndMergeLeafNodes(int maxNodeSize) { //check: We start with including all local values: nValues int nTotal = nValues; + int nSubs = 0; for (int i = 0; i < subs.length; i++) { Object e = subs[i]; if (e instanceof QNode) { @@ -328,10 +339,27 @@ private void checkAndMergeLeafNodes(int maxNodeSize) { //too many children return; } + nSubs++; } } - - //okay, let's merge + + //okay, let's merge. + // Special case: only one subnode (all subs are leaf nodes) + if (nSubs == 1) { + for (int i = 0; i < subs.length; i++) { + Object e = subs[i]; + if (e instanceof QNode) { + QNode sub = (QNode) e; + values = sub.values; + nValues = sub.nValues; + subs = null; + isLeaf = true; + return; + } + } + throw new IllegalStateException(); + } + values = new PointEntry[nTotal]; nValues = 0; for (int i = 0; i < subs.length; i++) { @@ -402,20 +430,33 @@ void checkNode(QStats s, QNode parent, int depth) { if (parent != null) { if (!QUtil.isNodeEnclosed(center, radius, parent.center, parent.radius*QUtil.EPS_MUL)) { + System.out.println("Outer: " + parent.radius + " " + Arrays.toString(parent.center)); + System.out.println("Child: " + radius + " " + Arrays.toString(center)); for (int d = 0; d < center.length; d++) { -// if ((centerOuter[d]+radiusOuter) / (centerEnclosed[d]+radiusEnclosed) < 0.9999999 || +// if ((centerOuter[d]+radiusOuter) / (centerEnclosed[d]+radiusEnclosed) < 0.9999999 || // (centerOuter[d]-radiusOuter) / (centerEnclosed[d]-radiusEnclosed) > 1.0000001) { // return false; // } - System.out.println("Outer: " + parent.radius + " " + - Arrays.toString(parent.center)); - System.out.println("Child: " + radius + " " + Arrays.toString(center)); - System.out.println((parent.center[d]+parent.radius) + " vs " + (center[d]+radius)); - System.out.println("r=" + (parent.center[d]+parent.radius) / (center[d]+radius)); - System.out.println((parent.center[d]-parent.radius) + " vs " + (center[d]-radius)); - System.out.println("r=" + (parent.center[d]-parent.radius) / (center[d]-radius)); + double parentMax = parent.center[d] + parent.radius; + double childMax = center[d] + radius; + double parentMin = parent.center[d] - parent.radius; + double childMin = center[d] - radius; + if (parentMax < childMax) { + System.out.println("DIM: " + d); + System.out.println("max: " + parentMax + " vs " + childMax); + System.out.println(" r: " + parentMax / childMax); + } + if (parentMin > childMin) { + System.out.println("DIM: " + d); + System.out.println("min: " + parentMin + " vs " + childMin); + System.out.println(" r: " + parentMin / childMin); + } + // System.out.println("max " + (parent.center[d]+parent.radius) + " vs " + (center[d]+radius)); + // System.out.println(" r=" + (parent.center[d]+parent.radius) / (center[d]+radius)); + // System.out.println("min " + (parent.center[d]-parent.radius) + " vs " + (center[d]-radius)); + // System.out.println(" r=" + (parent.center[d]-parent.radius) / (center[d]-radius)); } - throw new IllegalStateException(); + // throw new IllegalStateException(); // TODO reenable } } if (values != null) { @@ -429,6 +470,10 @@ void checkNode(QStats s, QNode parent, int depth) { if (subs != null) { throw new IllegalStateException(); } + if (nValues < 2 && parent != null) { + // Leaf nodes (except the root) must contain at least two values. + throw new IllegalStateException(); + } } else { s.nInner++; if (subs.length != 1L< e) { //extend upwards, even if extension unnecessary for this dimension. center2[d] = center[d]+radius; } + +// double parentMax = center2[d] + radius2 * QUtil.EPS_MUL; +// double childMax = center[d] + radius; +// double parentMin = center2[d] - radius2 * 1.000000001;// * QUtil.EPS_MUL; +// double childMin = center[d] - radius; +// if (parentMax < childMax) { +// System.out.println("ec DIM: " + d); +// System.out.println("ec max: " + (center2[d]+radius2*QUtil.EPS_MUL) + " vs " + (center[d]+radius)); +// System.out.println("ec r: " + (center2[d]+radius2*QUtil.EPS_MUL) / (center[d]+radius)); +// } +// if (parentMin > childMin) { +// System.out.println("ec DIM: " + d); +// System.out.println("ec min: " + parentMin + " vs " + childMin); +// System.out.println("ec r: " + parentMin / childMin); +// System.out.println("ec min: " + (center2[d]-radius2*QUtil.EPS_MUL) + " vs " + (center[d]-radius)); +// System.out.println("ec r: " + (center2[d]-radius2*QUtil.EPS_MUL) / (center[d]-radius)); +// } + } if (QuadTreeKD2.DEBUG && !QUtil.isNodeEnclosed(center, radius, center2, radius2)) { throw new IllegalStateException("e=" + Arrays.toString(e.point()) + diff --git a/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java b/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java new file mode 100644 index 0000000..20bf5e0 --- /dev/null +++ b/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java @@ -0,0 +1,41 @@ +package org.tinspin.index.qt2; + +import java.util.Arrays; + +import org.junit.Test; +import org.tinspin.index.qthypercube2.QuadTreeKD2; + + +public class Quadtree2Test { + + @Test + public void testIssue0040() { + double[][] data = new double[][] { + new double[]{-49.0949020385742, -2.05027413368225, 819588127, 0}, + new double[]{-49.0949020385742, -2.05027389526367, 819588127, 0}, + new double[]{-45.6938514709473, 32.9847145080566, -2056090140, 0}, + new double[]{-45.6938514709473, 32.9847145080566, -2056090140, 0}, + new double[]{-1.7595032453537, 112.097793579102, -267989921, 0}, + new double[]{-1.75950336456299, 112.097793579102, -267989921, 0}, + new double[]{45.6938438415527, 32.9847145080566, 1591613824, 0}, + new double[]{45.6938438415527, 32.9847145080566, 1591613824, 0}, + new double[]{49.0948944091797, -2.05027413368225, 14481734, 0}, + new double[]{49.0948944091797, -2.05027389526367, 14481734, 0}, + new double[]{-49.0949020385742, -2.05027413368225, 819588127, 1}, + new double[]{-49.0949020385742, -2.05027389526367, 819588127, 1}, + new double[]{-49.0949020385742, -2.05027413368225, 916603126, 0}, + }; + + QuadTreeKD2 tree = QuadTreeKD2.create(2); + for (int i = 0; i < data.length; i++) { + System.out.println("==================="); + System.out.println(tree.toStringTree()); + tree.getStats(); + if (data[i][3] == 0) { + tree.insert(Arrays.copyOf(data[i], 2), (int)data[i][2]); + } else { + tree.remove(Arrays.copyOf(data[i], 2), (int)data[i][2]); + } + } + } +} From d12001e2f57dec0e7a889b564bf0f720ae22e1af Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sun, 9 Jun 2024 16:00:52 +0200 Subject: [PATCH 02/12] Fix for issue #40 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de30935..0e01064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [2.1.4 - Unreleased] -- Fixed tree corruption after remove() in QT2. +- Fixed tree corruption after remove() in QT2. [#40](https://github.com/tzaeschke/tinspin-indexes/issue/40) ## [2.1.3] - 2023-11-19 From 5ace0aef9982942a54ad2b676b687c3eed1ee31d Mon Sep 17 00:00:00 2001 From: Tilmann Date: Thu, 13 Jun 2024 18:25:14 +0200 Subject: [PATCH 03/12] Fix for issue #40 --- CHANGELOG.md | 7 ++ src/main/java/org/tinspin/index/Index.java | 1 + .../org/tinspin/index/qthypercube2/QNode.java | 76 +++++++++---------- .../index/qthypercube2/QuadTreeKD2.java | 3 +- .../org/tinspin/index/qt2/Quadtree2Test.java | 56 +++++++++++++- .../org/tinspin/index/test/PointMapTest.java | 1 + .../tinspin/index/test/PointMultimapTest.java | 3 + 7 files changed, 103 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e01064..05f52a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [2.1.4 - Unreleased] - Fixed tree corruption after remove() in QT2. [#40](https://github.com/tzaeschke/tinspin-indexes/issue/40) + - Fixed tree consistency (single-entry leaf after remove) + - Fixed tree consistency (nValues) -> verify + - Fixed bug in qt2.contains() +TODO +- CLean up calls to checkMerge() -> call leaf-merge only in leaf! +- What is going on with root expansion? Why does it not fail in tests? +- check qt0 ## [2.1.3] - 2023-11-19 diff --git a/src/main/java/org/tinspin/index/Index.java b/src/main/java/org/tinspin/index/Index.java index 01cba75..5bed4d7 100644 --- a/src/main/java/org/tinspin/index/Index.java +++ b/src/main/java/org/tinspin/index/Index.java @@ -41,6 +41,7 @@ public interface Index { /** * @return Collect and return some index statistics. Note that indexes are not required * to fill all fields. Also, individual indexes may use subclasses with additional fields. + * Many indexes also perform consistency checks while gathering stats. */ Stats getStats(); diff --git a/src/main/java/org/tinspin/index/qthypercube2/QNode.java b/src/main/java/org/tinspin/index/qthypercube2/QNode.java index 71bf065..833f002 100644 --- a/src/main/java/org/tinspin/index/qthypercube2/QNode.java +++ b/src/main/java/org/tinspin/index/qthypercube2/QNode.java @@ -211,7 +211,9 @@ PointEntry remove(QNode parent, double[] key, int maxNodeSize, Predicate

removed = ((QNode)o).remove(this, key, maxNodeSize, pred); - checkAndMergeLeafNodesInParent(parent, maxNodeSize); + if (removed != null) { + checkAndMergeLeafNodesInParent(parent, maxNodeSize); + } return removed; } else if (o instanceof PointEntry) { PointEntry e = (PointEntry) o; @@ -284,7 +286,7 @@ PointEntry update(QNode parent, double[] keyOld, double[] keyNew, int maxN } return qe; } - throw new IllegalStateException(); + return null; } for (int i = 0; i < nValues; i++) { @@ -313,9 +315,25 @@ private void updateSub(double[] keyNew, PointEntry e, QNode parent, int ma } } + private boolean checkMergeSingleLeaf(QNode parent) { + // Merge single value into parent if possible + if (!isLeaf() || parent == null || nValues > 1) { + return false; + } + for (int i = 0; i < parent.subs.length; i++) { + if (parent.subs[i] == this) { + parent.subs[i] = values[0]; + parent.nValues++; + nValues = 0; + return true; + } + } + throw new IllegalStateException(); + } + @SuppressWarnings("unchecked") private void checkAndMergeLeafNodesInParent(QNode parent, int maxNodeSize) { - if (parent != null) { + if (!checkMergeSingleLeaf(parent) && parent != null) { parent.checkAndMergeLeafNodes(maxNodeSize); } } @@ -323,8 +341,7 @@ private void checkAndMergeLeafNodesInParent(QNode parent, int maxNodeSize) { @SuppressWarnings("unchecked") private void checkAndMergeLeafNodes(int maxNodeSize) { //check: We start with including all local values: nValues - int nTotal = nValues; - int nSubs = 0; + int nTotal = 0; for (int i = 0; i < subs.length; i++) { Object e = subs[i]; if (e instanceof QNode) { @@ -335,31 +352,16 @@ private void checkAndMergeLeafNodes(int maxNodeSize) { return; } nTotal += sub.getValueCount(); - if (nTotal > maxNodeSize) { - //too many children - return; - } - nSubs++; + } else if (e instanceof PointEntry) { + nTotal++; } } - - //okay, let's merge. - // Special case: only one subnode (all subs are leaf nodes) - if (nSubs == 1) { - for (int i = 0; i < subs.length; i++) { - Object e = subs[i]; - if (e instanceof QNode) { - QNode sub = (QNode) e; - values = sub.values; - nValues = sub.nValues; - subs = null; - isLeaf = true; - return; - } - } - throw new IllegalStateException(); + if (nTotal > maxNodeSize) { + //too many children + return; } + //okay, let's merge. values = new PointEntry[nTotal]; nValues = 0; for (int i = 0; i < subs.length; i++) { @@ -394,7 +396,7 @@ PointEntry getExact(double[] key, Predicate> pred) { return ((QNode)sub).getExact(key, pred); } else if (sub != null) { PointEntry e = (PointEntry) sub; - if (QUtil.isPointEqual(e.point(), key)) { + if (QUtil.isPointEqual(e.point(), key) && pred.test(e)) { return e; } } @@ -433,10 +435,6 @@ void checkNode(QStats s, QNode parent, int depth) { System.out.println("Outer: " + parent.radius + " " + Arrays.toString(parent.center)); System.out.println("Child: " + radius + " " + Arrays.toString(center)); for (int d = 0; d < center.length; d++) { -// if ((centerOuter[d]+radiusOuter) / (centerEnclosed[d]+radiusEnclosed) < 0.9999999 || -// (centerOuter[d]-radiusOuter) / (centerEnclosed[d]-radiusEnclosed) > 1.0000001) { -// return false; -// } double parentMax = parent.center[d] + parent.radius; double childMax = center[d] + radius; double parentMin = parent.center[d] - parent.radius; @@ -451,12 +449,8 @@ void checkNode(QStats s, QNode parent, int depth) { System.out.println("min: " + parentMin + " vs " + childMin); System.out.println(" r: " + parentMin / childMin); } - // System.out.println("max " + (parent.center[d]+parent.radius) + " vs " + (center[d]+radius)); - // System.out.println(" r=" + (parent.center[d]+parent.radius) / (center[d]+radius)); - // System.out.println("min " + (parent.center[d]-parent.radius) + " vs " + (center[d]-radius)); - // System.out.println(" r=" + (parent.center[d]-parent.radius) / (center[d]-radius)); } - // throw new IllegalStateException(); // TODO reenable + throw new IllegalStateException(); } } if (values != null) { @@ -480,6 +474,7 @@ void checkNode(QStats s, QNode parent, int depth) { throw new IllegalStateException(); } int nSubs = 0; + int nFoundValues = 0; for (int i = 0; i < subs.length; i++) { Object n = subs[i]; //TODO check pos @@ -488,9 +483,14 @@ void checkNode(QStats s, QNode parent, int depth) { ((QNode)n).checkNode(s, this, depth+1); } else if (n != null) { s.nEntries++; + nFoundValues++; checkEntry(n); } } + if (nValues != nFoundValues) { + throw new IllegalStateException(); + } + s.histoValues[nFoundValues]++; s.histo(nSubs); } } @@ -502,10 +502,6 @@ private void checkEntry(Object o) { System.out.println("Node: " + radius + " " + Arrays.toString(center)); System.out.println("Child: " + Arrays.toString(e.point())); for (int d = 0; d < center.length; d++) { -// if ((centerOuter[d]+radiusOuter) / (centerEnclosed[d]+radiusEnclosed) < 0.9999999 || -// (centerOuter[d]-radiusOuter) / (centerEnclosed[d]-radiusEnclosed) > 1.0000001) { -// return false; -// } System.out.println("min/max for " + d); System.out.println("min: " + (center[d]-radius) + " vs " + (e.point()[d])); System.out.println("r=" + (center[d]-radius) / (e.point()[d])); diff --git a/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java b/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java index d721dc3..362fc06 100644 --- a/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java +++ b/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java @@ -396,6 +396,7 @@ private void toStringTree(StringBuilderLn sb, QNode node, int depth, int posInParent) { String prefix = ".".repeat(depth); sb.append(prefix + posInParent + " d=" + depth); + sb.append(" nV=" + node.getValueCount()); sb.append(" " + Arrays.toString(node.getCenter())); sb.appendLn("/" + node.getRadius()); prefix += " "; @@ -436,7 +437,7 @@ public QStats getStats() { * Statistics container class. */ public static class QStats extends Stats { - final int[] histoValues = new int[100]; + final int[] histoValues = new int[1000]; final int[] histoSubs; static final int HISTO_MAX = (1 << 10) + 1; public QStats(int dims) { diff --git a/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java b/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java index 20bf5e0..6c4e2a7 100644 --- a/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java +++ b/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java @@ -1,15 +1,35 @@ +/* + * Copyright 2009-2017 Tilmann Zaeschke. All rights reserved. + * + * This file is part of TinSpin. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.tinspin.index.qt2; -import java.util.Arrays; - import org.junit.Test; import org.tinspin.index.qthypercube2.QuadTreeKD2; +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + public class Quadtree2Test { @Test - public void testIssue0040() { + public void testIssue0040_remove() { double[][] data = new double[][] { new double[]{-49.0949020385742, -2.05027413368225, 819588127, 0}, new double[]{-49.0949020385742, -2.05027389526367, 819588127, 0}, @@ -38,4 +58,34 @@ public void testIssue0040() { } } } + + @Test + public void testIssue0040_rootExpansion() { + double[][] data = new double[][] { + new double[]{-49.0949020385742, -2.05027413368225, 819588127, 0}, + new double[]{-49.0949020385742, -2.05027389526367, 819588127, 0}, + new double[]{-45.6938514709473, 32.9847145080566, -2056090140, 0}, + new double[]{-45.6938514709473, 32.9847145080566, -2056090140, 0}, + new double[]{-1.7595032453537, 112.097793579102, -267989921, 0}, + new double[]{-1.75950336456299, 112.097793579102, -267989921, 0}, + new double[]{45.6938438415527, 32.9847145080566, 1591613824, 0}, + new double[]{45.6938438415527, 32.9847145080566, 1591613824, 0}, + new double[]{49.0948944091797, -2.05027413368225, 14481734, 0}, + new double[]{49.0948944091797, -2.05027389526367, 14481734, 0}, + }; + + QuadTreeKD2 tree = QuadTreeKD2.create(2); + for (int i = 0; i < data.length; i++) { + tree.getStats(); + tree.insert(Arrays.copyOf(data[i], 2), (int)data[i][2]); + } + System.out.println(tree.toStringTree()); + + // root test + for (int i = 0; i < data.length; i++) { + assertTrue(tree.contains(data[i])); + assertTrue(tree.contains(data[i], (int)data[i][2])); + assertEquals( (int)data[i][2], (int) tree.queryExact(data[i])); + } + } } diff --git a/src/test/java/org/tinspin/index/test/PointMapTest.java b/src/test/java/org/tinspin/index/test/PointMapTest.java index 5a0da7c..224a0de 100644 --- a/src/test/java/org/tinspin/index/test/PointMapTest.java +++ b/src/test/java/org/tinspin/index/test/PointMapTest.java @@ -127,6 +127,7 @@ private void smokeTest(List data) { tree.insert(e.p, e); } // System.out.println(tree.toStringTree()); + tree.getStats(); for (Entry e : data) { assertTrue("contains(point) failed: " + e, tree.contains(e.p)); Entry e2 = tree.queryExact(e.p); diff --git a/src/test/java/org/tinspin/index/test/PointMultimapTest.java b/src/test/java/org/tinspin/index/test/PointMultimapTest.java index f8f21a0..025d043 100644 --- a/src/test/java/org/tinspin/index/test/PointMultimapTest.java +++ b/src/test/java/org/tinspin/index/test/PointMultimapTest.java @@ -211,6 +211,7 @@ public void testUpdate() { assertFalse(containsExact(tree, pOld, e.id)); assertTrue(containsExact(tree, e.p, e.id)); } + tree.getStats(); for (int i = 0; i < data.size(); ++i) { Entry e = data.get(i); @@ -327,6 +328,7 @@ public void testIssueKnnRemove() { for (Entry e : data) { tree.insert(e.p, e); } + tree.getStats(); // remove 1st half for (int i = 0; i < nDelete; ++i) { @@ -336,6 +338,7 @@ public void testIssueKnnRemove() { assertFalse(containsExact(tree, e.p, e.id)); assertFalse(tree.contains(e.p, e)); } + tree.getStats(); // check contains() & kNN for (int i = 0; i < nDelete; ++i) { From c6e775b4cee35b10728dc7fed5c93b71a074b4ed Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sat, 22 Jun 2024 15:21:27 +0200 Subject: [PATCH 04/12] Fix for issue #40 --- .../tinspin/index/test/PointMultimapTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/test/java/org/tinspin/index/test/PointMultimapTest.java b/src/test/java/org/tinspin/index/test/PointMultimapTest.java index 025d043..e758e3e 100644 --- a/src/test/java/org/tinspin/index/test/PointMultimapTest.java +++ b/src/test/java/org/tinspin/index/test/PointMultimapTest.java @@ -420,4 +420,42 @@ public String toString() { return "id=" + id + ":" + Arrays.toString(p); } } + + @Test + public void testIssue0040_remove() { + double[][] data = new double[][] { + new double[]{-49.0949020385742, -2.05027413368225, 819588127, 0}, + new double[]{-49.0949020385742, -2.05027389526367, 819588127, 0}, + new double[]{-45.6938514709473, 32.9847145080566, -2056090140, 0}, + new double[]{-45.6938514709473, 32.9847145080566, -2056090140, 0}, + new double[]{-1.7595032453537, 112.097793579102, -267989921, 0}, + new double[]{-1.75950336456299, 112.097793579102, -267989921, 0}, + new double[]{45.6938438415527, 32.9847145080566, 1591613824, 0}, + new double[]{45.6938438415527, 32.9847145080566, 1591613824, 0}, + new double[]{49.0948944091797, -2.05027413368225, 14481734, 0}, + new double[]{49.0948944091797, -2.05027389526367, 14481734, 0}, + new double[]{-49.0949020385742, -2.05027413368225, 819588127, 1}, + new double[]{-49.0949020385742, -2.05027389526367, 819588127, 1}, + new double[]{-49.0949020385742, -2.05027413368225, 916603126, 0}, + }; + + PointMultimap tree = createTree(100,2); + for (int i = 0; i < data.length; i++) { + if (data[i][3] == 0) { + tree.insert(Arrays.copyOf(data[i], 2), (int)data[i][2]); + } else { + tree.remove(Arrays.copyOf(data[i], 2), (int)data[i][2]); + } + } + + assertEquals(9, tree.size()); + int n = 0; + double[] min = new double[]{-50, -3}; + double[] max = new double[]{50, 113}; + for (Index.PointIterator it = tree.query(min, max); it.hasNext(); it.next()) { + n++; + } + assertEquals(9, n); + } + } From 93df62ced01c4eeb8b050c7659c3ce29b5568bfd Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sat, 22 Jun 2024 15:26:48 +0200 Subject: [PATCH 05/12] Fix for issue #42 --- CHANGELOG.md | 10 +++++----- src/test/java/org/tinspin/index/qt2/Quadtree2Test.java | 10 ++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05f52a8..5bc680f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,14 +10,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [2.1.4 - Unreleased] -- Fixed tree corruption after remove() in QT2. [#40](https://github.com/tzaeschke/tinspin-indexes/issue/40) +- Fixed tree corruption after remove() in QT2. [#40](https://github.com/tzaeschke/tinspin-indexes/issues/40) - Fixed tree consistency (single-entry leaf after remove) - Fixed tree consistency (nValues) -> verify - Fixed bug in qt2.contains() -TODO -- CLean up calls to checkMerge() -> call leaf-merge only in leaf! -- What is going on with root expansion? Why does it not fail in tests? -- check qt0 +- Fixed tree inconsistency after root resizing after insert(). [#42](https://github.com/tzaeschke/tinspin-indexes/issues/42) + - CLean up calls to checkMerge() -> call leaf-merge only in leaf! + - What is going on with root expansion? Why does it not fail in tests? + - check qt0 ## [2.1.3] - 2023-11-19 diff --git a/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java b/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java index 6c4e2a7..1668464 100644 --- a/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java +++ b/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java @@ -18,6 +18,7 @@ package org.tinspin.index.qt2; import org.junit.Test; +import org.tinspin.index.Index; import org.tinspin.index.qthypercube2.QuadTreeKD2; import java.util.Arrays; @@ -57,6 +58,15 @@ public void testIssue0040_remove() { tree.remove(Arrays.copyOf(data[i], 2), (int)data[i][2]); } } + + assertEquals(9, tree.size()); + int n = 0; + double[] min = new double[]{-50, -3}; + double[] max = new double[]{50, 113}; + for (Index.PointIterator it = tree.query(min, max); it.hasNext(); it.next()) { + n++; + } + assertEquals(9, n); } @Test From fdb6c7e79762347bcec0279bf366d6a0ffc58e35 Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sun, 23 Jun 2024 15:44:59 +0200 Subject: [PATCH 06/12] Fix root-node precision problems --- CHANGELOG.md | 4 +- .../org/tinspin/index/qthypercube2/QNode.java | 2 +- .../index/qthypercube2/QuadTreeKD2.java | 43 ++++++++++++++++--- .../tinspin/index/test/PointMultimapTest.java | 7 +++ 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bc680f..5d36b78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Fixed tree consistency (nValues) -> verify - Fixed bug in qt2.contains() - Fixed tree inconsistency after root resizing after insert(). [#42](https://github.com/tzaeschke/tinspin-indexes/issues/42) - - CLean up calls to checkMerge() -> call leaf-merge only in leaf! - - What is going on with root expansion? Why does it not fail in tests? + Essentially, we enforce all radii and thecenter of the root to be a power of two. + This should immensely reduce and problems with precision errors. - check qt0 ## [2.1.3] - 2023-11-19 diff --git a/src/main/java/org/tinspin/index/qthypercube2/QNode.java b/src/main/java/org/tinspin/index/qthypercube2/QNode.java index 833f002..306fd36 100644 --- a/src/main/java/org/tinspin/index/qthypercube2/QNode.java +++ b/src/main/java/org/tinspin/index/qthypercube2/QNode.java @@ -433,7 +433,7 @@ void checkNode(QStats s, QNode parent, int depth) { if (parent != null) { if (!QUtil.isNodeEnclosed(center, radius, parent.center, parent.radius*QUtil.EPS_MUL)) { System.out.println("Outer: " + parent.radius + " " + Arrays.toString(parent.center)); - System.out.println("Child: " + radius + " " + Arrays.toString(center)); + System.out.println("Child(" + depth + "): " + radius + " " + Arrays.toString(center)); for (int d = 0; d < center.length; d++) { double parentMax = parent.center[d] + parent.radius; double childMax = center[d] + radius; diff --git a/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java b/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java index 362fc06..e0e3d65 100644 --- a/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java +++ b/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java @@ -108,7 +108,7 @@ public void insert(double[] key, T value) { PointEntry e = new PointEntry<>(key, value); if (root == null) { // We calculate a better radius when adding a second point. - root = new QNode<>(key.clone(), INITIAL_RADIUS); + root = new QNode<>(normalizeCopy(key), INITIAL_RADIUS); } if (root.getRadius() == INITIAL_RADIUS) { adjustRootSize(key); @@ -127,15 +127,34 @@ private void adjustRootSize(double[] key) { return; } if (root.getRadius() == INITIAL_RADIUS) { - double dist = PointDistance.L2.dist(key, root.getCenter()); - if (dist > 0) { - root.adjustRadius(2 * dist); + double dMax = maxOrthoDistance(key, root.getCenter()); + for (int i = 0; i < root.getValueCount(); i++) { + dMax = Math.max(dMax, maxOrthoDistance(root.getValues()[i].point(), root.getCenter())); + } + double radius = normalize(dMax) * 2; + if (radius > 0) { + root.adjustRadius(radius); } else if (root.getValueCount() >= maxNodeSize - 1) { - // we just set an arbitrary radius here + // all entries have (approximately?) the same coordinates. We just set an arbitrary radius here. root.adjustRadius(1000); } +// double dist = PointDistance.L2.dist(key, root.getCenter()); +// if (dist > 0) { +// root.adjustRadius(2 * dist); +// } else if (root.getValueCount() >= maxNodeSize - 1) { +// // we just set an arbitrary radius here +// root.adjustRadius(1000); +// } } } + + private static double maxOrthoDistance(double[] v1, double[] v2) { + double dMax = 0; + for (int i = 0; i < v1.length; i++) { + dMax = Math.max(dMax, Math.abs(v1[i] - v2[i])); + } + return dMax; + } /** * Check whether a given key exists. @@ -260,6 +279,20 @@ public T updateIf(double[] oldKey, double[] newKey, Predicate> con return e.value(); } + private static double normalize(double d) { + // Set fraction to "0". + return Double.longBitsToDouble(Double.doubleToRawLongBits(d) & 0xFFF0_0000_0000_0000L); + } + + private static double[] normalizeCopy(double[] d) { + double[] d2 = new double[d.length]; + for (int i = 0; i < d.length; i++) { + d2[i] = normalize(d[i]); + } + // System.out.println("NORM: " + Arrays.toString(d) + " -> " + Arrays.toString(d2)); + return d2; + } + /** * Ensure that the tree covers the entry. * @param e Entry to cover. diff --git a/src/test/java/org/tinspin/index/test/PointMultimapTest.java b/src/test/java/org/tinspin/index/test/PointMultimapTest.java index e758e3e..9f67494 100644 --- a/src/test/java/org/tinspin/index/test/PointMultimapTest.java +++ b/src/test/java/org/tinspin/index/test/PointMultimapTest.java @@ -141,6 +141,10 @@ private void smokeTest(List data) { for (Entry e : data) { tree.insert(e.p, e); } + + // Check consistency + tree.getStats(); + // System.out.println(tree.toStringTree()); for (Entry e : data) { PointIterator it = tree.queryExactPoint(e.p); @@ -167,6 +171,9 @@ private void smokeTest(List data) { assertEquals(data.size(), nExtent); } + //System.out.println(tree.toStringTree()); + tree.getStats(); + for (Entry e : data) { // System.out.println("query: " + Arrays.toString(e.p)); PointIterator iter = tree.query(e.p, e.p); From cd1648786d7411aad0e80d2135821f180773c853 Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sat, 29 Jun 2024 18:30:52 +0200 Subject: [PATCH 07/12] Fix root-node precision problems --- CHANGELOG.md | 3 +- src/main/java/org/tinspin/index/Stats.java | 5 ++- .../org/tinspin/index/qthypercube/QNode.java | 1 + .../org/tinspin/index/qthypercube/QRNode.java | 1 + .../tinspin/index/qthypercube/QuadTreeKD.java | 2 +- .../org/tinspin/index/qthypercube2/QNode.java | 1 + .../index/qthypercube2/QuadTreeKD2.java | 44 ++----------------- .../java/org/tinspin/index/qtplain/QNode.java | 1 + .../org/tinspin/index/qtplain/QRNode.java | 1 + .../java/org/tinspin/index/util/BitTools.java | 35 +++++++++++++++ .../org/tinspin/index/qt2/Quadtree2Test.java | 6 +-- 11 files changed, 52 insertions(+), 48 deletions(-) create mode 100644 src/main/java/org/tinspin/index/util/BitTools.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d36b78..eb8d60d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Fixed tree consistency (nValues) -> verify - Fixed bug in qt2.contains() - Fixed tree inconsistency after root resizing after insert(). [#42](https://github.com/tzaeschke/tinspin-indexes/issues/42) - Essentially, we enforce all radii and thecenter of the root to be a power of two. + Essentially, we enforce all radii and the center of the root to be a power of two. This should immensely reduce and problems with precision errors. - - check qt0 ## [2.1.3] - 2023-11-19 diff --git a/src/main/java/org/tinspin/index/Stats.java b/src/main/java/org/tinspin/index/Stats.java index 5270d51..312739b 100644 --- a/src/main/java/org/tinspin/index/Stats.java +++ b/src/main/java/org/tinspin/index/Stats.java @@ -27,6 +27,7 @@ public class Stats { public int minLevel = Integer.MAX_VALUE; public int maxLevel = -1; public int maxDepth = 0; + public int maxValuesInNode = 0; public double sumLevel; public int maxNodeSize = -1; public int nLeaf; @@ -49,7 +50,9 @@ public String toString() { ";nNodes=" + nNodes + ";nLeaf=" + nLeaf + ";nInner=" + nInner + - ";minLevel=" + minLevel + + ";maxDepth=" + maxDepth + + ";maxValues=" + maxValuesInNode + + ";minLevel=" + minLevel + ";maxLevel=" + maxLevel + ";avgLevel=" + (sumLevel/nEntries) + ";maxNodeSize=" + maxNodeSize; diff --git a/src/main/java/org/tinspin/index/qthypercube/QNode.java b/src/main/java/org/tinspin/index/qthypercube/QNode.java index b2e8d39..393e3ad 100644 --- a/src/main/java/org/tinspin/index/qthypercube/QNode.java +++ b/src/main/java/org/tinspin/index/qthypercube/QNode.java @@ -303,6 +303,7 @@ void checkNode(QStats s, QNode parent, int depth) { s.nLeaf++; s.nEntries += values.size(); s.histoValues[values.size()]++; + s.maxValuesInNode = Math.max(s.maxValuesInNode, values.size()); for (int i = 0; i < values.size(); i++) { PointEntry e = values.get(i); if (!QUtil.fitsIntoNode(e.point(), center, radius*QUtil.EPS_MUL)) { diff --git a/src/main/java/org/tinspin/index/qthypercube/QRNode.java b/src/main/java/org/tinspin/index/qthypercube/QRNode.java index 9a297a6..d07cda3 100644 --- a/src/main/java/org/tinspin/index/qthypercube/QRNode.java +++ b/src/main/java/org/tinspin/index/qthypercube/QRNode.java @@ -350,6 +350,7 @@ void checkNode(QStats s, QRNode parent, int depth) { } } if (values != null) { + s.maxValuesInNode = Math.max(s.maxValuesInNode, values.size()); for (int i = 0; i < values.size(); i++) { BoxEntry e = values.get(i); if (!QUtil.fitsIntoNode(e.min(), e.max(), center, radius*QUtil.EPS_MUL)) { diff --git a/src/main/java/org/tinspin/index/qthypercube/QuadTreeKD.java b/src/main/java/org/tinspin/index/qthypercube/QuadTreeKD.java index 575ead4..cbe1993 100644 --- a/src/main/java/org/tinspin/index/qthypercube/QuadTreeKD.java +++ b/src/main/java/org/tinspin/index/qthypercube/QuadTreeKD.java @@ -407,7 +407,7 @@ public QStats getStats() { * Statistics container class. */ public static class QStats extends Stats { - final int[] histoValues = new int[100]; + final int[] histoValues = new int[1000]; final int[] histoSubs; public QStats(int dims) { diff --git a/src/main/java/org/tinspin/index/qthypercube2/QNode.java b/src/main/java/org/tinspin/index/qthypercube2/QNode.java index 306fd36..53150ea 100644 --- a/src/main/java/org/tinspin/index/qthypercube2/QNode.java +++ b/src/main/java/org/tinspin/index/qthypercube2/QNode.java @@ -457,6 +457,7 @@ void checkNode(QStats s, QNode parent, int depth) { s.nLeaf++; s.nEntries += nValues; s.histoValues[nValues]++; + s.maxValuesInNode = Math.max(s.maxValuesInNode, nValues); for (int i = 0; i < nValues; i++) { PointEntry e = values[i]; checkEntry(e); diff --git a/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java b/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java index e0e3d65..563aeec 100644 --- a/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java +++ b/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java @@ -21,6 +21,7 @@ import java.util.function.Predicate; import org.tinspin.index.*; +import org.tinspin.index.util.BitTools; import org.tinspin.index.util.StringBuilderLn; /** @@ -108,7 +109,7 @@ public void insert(double[] key, T value) { PointEntry e = new PointEntry<>(key, value); if (root == null) { // We calculate a better radius when adding a second point. - root = new QNode<>(normalizeCopy(key), INITIAL_RADIUS); + root = new QNode<>(BitTools.normalizeCopy(key), INITIAL_RADIUS); } if (root.getRadius() == INITIAL_RADIUS) { adjustRootSize(key); @@ -131,20 +132,13 @@ private void adjustRootSize(double[] key) { for (int i = 0; i < root.getValueCount(); i++) { dMax = Math.max(dMax, maxOrthoDistance(root.getValues()[i].point(), root.getCenter())); } - double radius = normalize(dMax) * 2; + double radius = BitTools.normalize(dMax) * 2; if (radius > 0) { root.adjustRadius(radius); } else if (root.getValueCount() >= maxNodeSize - 1) { // all entries have (approximately?) the same coordinates. We just set an arbitrary radius here. root.adjustRadius(1000); } -// double dist = PointDistance.L2.dist(key, root.getCenter()); -// if (dist > 0) { -// root.adjustRadius(2 * dist); -// } else if (root.getValueCount() >= maxNodeSize - 1) { -// // we just set an arbitrary radius here -// root.adjustRadius(1000); -// } } } @@ -279,20 +273,6 @@ public T updateIf(double[] oldKey, double[] newKey, Predicate> con return e.value(); } - private static double normalize(double d) { - // Set fraction to "0". - return Double.longBitsToDouble(Double.doubleToRawLongBits(d) & 0xFFF0_0000_0000_0000L); - } - - private static double[] normalizeCopy(double[] d) { - double[] d2 = new double[d.length]; - for (int i = 0; i < d.length; i++) { - d2[i] = normalize(d[i]); - } - // System.out.println("NORM: " + Arrays.toString(d) + " -> " + Arrays.toString(d2)); - return d2; - } - /** * Ensure that the tree covers the entry. * @param e Entry to cover. @@ -317,24 +297,6 @@ private void ensureCoverage(PointEntry e) { //extend upwards, even if extension unnecessary for this dimension. center2[d] = center[d]+radius; } - -// double parentMax = center2[d] + radius2 * QUtil.EPS_MUL; -// double childMax = center[d] + radius; -// double parentMin = center2[d] - radius2 * 1.000000001;// * QUtil.EPS_MUL; -// double childMin = center[d] - radius; -// if (parentMax < childMax) { -// System.out.println("ec DIM: " + d); -// System.out.println("ec max: " + (center2[d]+radius2*QUtil.EPS_MUL) + " vs " + (center[d]+radius)); -// System.out.println("ec r: " + (center2[d]+radius2*QUtil.EPS_MUL) / (center[d]+radius)); -// } -// if (parentMin > childMin) { -// System.out.println("ec DIM: " + d); -// System.out.println("ec min: " + parentMin + " vs " + childMin); -// System.out.println("ec r: " + parentMin / childMin); -// System.out.println("ec min: " + (center2[d]-radius2*QUtil.EPS_MUL) + " vs " + (center[d]-radius)); -// System.out.println("ec r: " + (center2[d]-radius2*QUtil.EPS_MUL) / (center[d]-radius)); -// } - } if (QuadTreeKD2.DEBUG && !QUtil.isNodeEnclosed(center, radius, center2, radius2)) { throw new IllegalStateException("e=" + Arrays.toString(e.point()) + diff --git a/src/main/java/org/tinspin/index/qtplain/QNode.java b/src/main/java/org/tinspin/index/qtplain/QNode.java index 6c386e1..2a801fe 100644 --- a/src/main/java/org/tinspin/index/qtplain/QNode.java +++ b/src/main/java/org/tinspin/index/qtplain/QNode.java @@ -296,6 +296,7 @@ void checkNode(QStats s, QNode parent, int depth) { } } if (values != null) { + s.maxValuesInNode = Math.max(s.maxValuesInNode, values.size()); for (int i = 0; i < values.size(); i++) { PointEntry e = values.get(i); if (!QUtil.fitsIntoNode(e.point(), center, radius*QUtil.EPS_MUL)) { diff --git a/src/main/java/org/tinspin/index/qtplain/QRNode.java b/src/main/java/org/tinspin/index/qtplain/QRNode.java index bdf942d..766b5c8 100644 --- a/src/main/java/org/tinspin/index/qtplain/QRNode.java +++ b/src/main/java/org/tinspin/index/qtplain/QRNode.java @@ -389,6 +389,7 @@ void checkNode(QStats s, QRNode parent, int depth) { } } if (values != null) { + s.maxValuesInNode = Math.max(s.maxValuesInNode, values.size()); for (int i = 0; i < values.size(); i++) { BoxEntry e = values.get(i); if (!QUtil.fitsIntoNode(e.min(), e.max(), center, radius*QUtil.EPS_MUL)) { diff --git a/src/main/java/org/tinspin/index/util/BitTools.java b/src/main/java/org/tinspin/index/util/BitTools.java new file mode 100644 index 0000000..0d21f89 --- /dev/null +++ b/src/main/java/org/tinspin/index/util/BitTools.java @@ -0,0 +1,35 @@ +/* + * Copyright 2016-2024 Tilmann Zaeschke + * + * This file is part of TinSpin. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */package org.tinspin.index.util; + +public class BitTools { + + public static double normalize(double d) { + // Set fraction to "0". + return Double.longBitsToDouble(Double.doubleToRawLongBits(d) & 0xFFF0_0000_0000_0000L); + } + + public static double[] normalizeCopy(double[] d) { + double[] d2 = new double[d.length]; + for (int i = 0; i < d.length; i++) { + d2[i] = normalize(d[i]); + } + // System.out.println("NORM: " + Arrays.toString(d) + " -> " + Arrays.toString(d2)); + return d2; + } + +} diff --git a/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java b/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java index 1668464..acd8ac6 100644 --- a/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java +++ b/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java @@ -49,8 +49,8 @@ public void testIssue0040_remove() { QuadTreeKD2 tree = QuadTreeKD2.create(2); for (int i = 0; i < data.length; i++) { - System.out.println("==================="); - System.out.println(tree.toStringTree()); + // System.out.println("==================="); + // System.out.println(tree.toStringTree()); tree.getStats(); if (data[i][3] == 0) { tree.insert(Arrays.copyOf(data[i], 2), (int)data[i][2]); @@ -89,7 +89,7 @@ public void testIssue0040_rootExpansion() { tree.getStats(); tree.insert(Arrays.copyOf(data[i], 2), (int)data[i][2]); } - System.out.println(tree.toStringTree()); + // System.out.println(tree.toStringTree()); // root test for (int i = 0; i < data.length; i++) { From a6ad42f08070a2cbe6dabc1668018ec07cbba134 Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sat, 29 Jun 2024 18:35:38 +0200 Subject: [PATCH 08/12] Fix root-node precision problems --- .../org/tinspin/index/qt2/Quadtree2Test.java | 101 ------------------ .../tinspin/index/test/PointMultimapTest.java | 20 +++- 2 files changed, 19 insertions(+), 102 deletions(-) delete mode 100644 src/test/java/org/tinspin/index/qt2/Quadtree2Test.java diff --git a/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java b/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java deleted file mode 100644 index acd8ac6..0000000 --- a/src/test/java/org/tinspin/index/qt2/Quadtree2Test.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2009-2017 Tilmann Zaeschke. All rights reserved. - * - * This file is part of TinSpin. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.tinspin.index.qt2; - -import org.junit.Test; -import org.tinspin.index.Index; -import org.tinspin.index.qthypercube2.QuadTreeKD2; - -import java.util.Arrays; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - - -public class Quadtree2Test { - - @Test - public void testIssue0040_remove() { - double[][] data = new double[][] { - new double[]{-49.0949020385742, -2.05027413368225, 819588127, 0}, - new double[]{-49.0949020385742, -2.05027389526367, 819588127, 0}, - new double[]{-45.6938514709473, 32.9847145080566, -2056090140, 0}, - new double[]{-45.6938514709473, 32.9847145080566, -2056090140, 0}, - new double[]{-1.7595032453537, 112.097793579102, -267989921, 0}, - new double[]{-1.75950336456299, 112.097793579102, -267989921, 0}, - new double[]{45.6938438415527, 32.9847145080566, 1591613824, 0}, - new double[]{45.6938438415527, 32.9847145080566, 1591613824, 0}, - new double[]{49.0948944091797, -2.05027413368225, 14481734, 0}, - new double[]{49.0948944091797, -2.05027389526367, 14481734, 0}, - new double[]{-49.0949020385742, -2.05027413368225, 819588127, 1}, - new double[]{-49.0949020385742, -2.05027389526367, 819588127, 1}, - new double[]{-49.0949020385742, -2.05027413368225, 916603126, 0}, - }; - - QuadTreeKD2 tree = QuadTreeKD2.create(2); - for (int i = 0; i < data.length; i++) { - // System.out.println("==================="); - // System.out.println(tree.toStringTree()); - tree.getStats(); - if (data[i][3] == 0) { - tree.insert(Arrays.copyOf(data[i], 2), (int)data[i][2]); - } else { - tree.remove(Arrays.copyOf(data[i], 2), (int)data[i][2]); - } - } - - assertEquals(9, tree.size()); - int n = 0; - double[] min = new double[]{-50, -3}; - double[] max = new double[]{50, 113}; - for (Index.PointIterator it = tree.query(min, max); it.hasNext(); it.next()) { - n++; - } - assertEquals(9, n); - } - - @Test - public void testIssue0040_rootExpansion() { - double[][] data = new double[][] { - new double[]{-49.0949020385742, -2.05027413368225, 819588127, 0}, - new double[]{-49.0949020385742, -2.05027389526367, 819588127, 0}, - new double[]{-45.6938514709473, 32.9847145080566, -2056090140, 0}, - new double[]{-45.6938514709473, 32.9847145080566, -2056090140, 0}, - new double[]{-1.7595032453537, 112.097793579102, -267989921, 0}, - new double[]{-1.75950336456299, 112.097793579102, -267989921, 0}, - new double[]{45.6938438415527, 32.9847145080566, 1591613824, 0}, - new double[]{45.6938438415527, 32.9847145080566, 1591613824, 0}, - new double[]{49.0948944091797, -2.05027413368225, 14481734, 0}, - new double[]{49.0948944091797, -2.05027389526367, 14481734, 0}, - }; - - QuadTreeKD2 tree = QuadTreeKD2.create(2); - for (int i = 0; i < data.length; i++) { - tree.getStats(); - tree.insert(Arrays.copyOf(data[i], 2), (int)data[i][2]); - } - // System.out.println(tree.toStringTree()); - - // root test - for (int i = 0; i < data.length; i++) { - assertTrue(tree.contains(data[i])); - assertTrue(tree.contains(data[i], (int)data[i][2])); - assertEquals( (int)data[i][2], (int) tree.queryExact(data[i])); - } - } -} diff --git a/src/test/java/org/tinspin/index/test/PointMultimapTest.java b/src/test/java/org/tinspin/index/test/PointMultimapTest.java index 9f67494..f02e1e9 100644 --- a/src/test/java/org/tinspin/index/test/PointMultimapTest.java +++ b/src/test/java/org/tinspin/index/test/PointMultimapTest.java @@ -17,11 +17,14 @@ */ package org.tinspin.index.test; +import ch.ethz.globis.phtree.util.PhTreeStats; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.tinspin.index.Index; import org.tinspin.index.PointMultimap; +import org.tinspin.index.Stats; +import org.tinspin.index.qthypercube.QuadTreeKD; import org.tinspin.index.test.util.TestInstances; import java.util.*; @@ -138,12 +141,27 @@ private void smokeTest(List data) { int dim = data.get(0).p.length; PointMultimap tree = createTree(data.size(), dim); + int x = 0; for (Entry e : data) { + //System.out.println("x=" + x++); + x++; tree.insert(e.p, e); + try { + tree.getStats(); + } catch (RuntimeException e2) { + System.out.println("x=" + x++); + System.out.println(tree.toStringTree()); + throw e2; + } } // Check consistency - tree.getStats(); + Stats s = tree.getStats(); + System.out.println(tree.getClass().getName() + " " + s.toString()); +// if (s instanceof QuadTreeKD.QStats) { +// System.out.println( " " + Arrays.toString(((QuadTreeKD.QStats)s).histoString()); +// } + //System.out.println(tree.toStringTree()); // System.out.println(tree.toStringTree()); for (Entry e : data) { From 9c046017552310747eb94f0f82d637814d886ebb Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sun, 30 Jun 2024 13:09:31 +0200 Subject: [PATCH 09/12] Fix root-node precision problems --- CHANGELOG.md | 2 +- .../index/qthypercube2/QuadTreeKD2.java | 6 +-- .../util/{BitTools.java => MathTools.java} | 27 ++++++++--- .../tinspin/index/test/PointMultimapTest.java | 24 +--------- .../java/org/tinspin/util/MathToolsTest.java | 48 +++++++++++++++++++ .../org/tinspin/{ => util}/MinHeapTest.java | 2 +- .../tinspin/{ => util}/MinMaxHeapTest.java | 2 +- 7 files changed, 76 insertions(+), 35 deletions(-) rename src/main/java/org/tinspin/index/util/{BitTools.java => MathTools.java} (54%) create mode 100644 src/test/java/org/tinspin/util/MathToolsTest.java rename src/test/java/org/tinspin/{ => util}/MinHeapTest.java (99%) rename src/test/java/org/tinspin/{ => util}/MinMaxHeapTest.java (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb8d60d..9372f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Fixed tree consistency (single-entry leaf after remove) - Fixed tree consistency (nValues) -> verify - Fixed bug in qt2.contains() -- Fixed tree inconsistency after root resizing after insert(). [#42](https://github.com/tzaeschke/tinspin-indexes/issues/42) +- Fixed QT2 inconsistency after root resizing after insert(). [#42](https://github.com/tzaeschke/tinspin-indexes/issues/42) Essentially, we enforce all radii and the center of the root to be a power of two. This should immensely reduce and problems with precision errors. diff --git a/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java b/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java index 563aeec..43ec401 100644 --- a/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java +++ b/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java @@ -21,7 +21,7 @@ import java.util.function.Predicate; import org.tinspin.index.*; -import org.tinspin.index.util.BitTools; +import org.tinspin.index.util.MathTools; import org.tinspin.index.util.StringBuilderLn; /** @@ -109,7 +109,7 @@ public void insert(double[] key, T value) { PointEntry e = new PointEntry<>(key, value); if (root == null) { // We calculate a better radius when adding a second point. - root = new QNode<>(BitTools.normalizeCopy(key), INITIAL_RADIUS); + root = new QNode<>(MathTools.floorPowerOfTwoCopy(key), INITIAL_RADIUS); } if (root.getRadius() == INITIAL_RADIUS) { adjustRootSize(key); @@ -132,7 +132,7 @@ private void adjustRootSize(double[] key) { for (int i = 0; i < root.getValueCount(); i++) { dMax = Math.max(dMax, maxOrthoDistance(root.getValues()[i].point(), root.getCenter())); } - double radius = BitTools.normalize(dMax) * 2; + double radius = MathTools.floorPowerOfTwo(dMax) * 2; if (radius > 0) { root.adjustRadius(radius); } else if (root.getValueCount() >= maxNodeSize - 1) { diff --git a/src/main/java/org/tinspin/index/util/BitTools.java b/src/main/java/org/tinspin/index/util/MathTools.java similarity index 54% rename from src/main/java/org/tinspin/index/util/BitTools.java rename to src/main/java/org/tinspin/index/util/MathTools.java index 0d21f89..a866f7d 100644 --- a/src/main/java/org/tinspin/index/util/BitTools.java +++ b/src/main/java/org/tinspin/index/util/MathTools.java @@ -14,22 +14,35 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */package org.tinspin.index.util; + */ +package org.tinspin.index.util; -public class BitTools { +public class MathTools { - public static double normalize(double d) { + private MathTools() {} + + /** + * Similar to Math.floor() with the floor being the next lower power of 2. + * The resulting number can repeatedly (almost) always be divided by two without loss of precision. + * @param d input + * @return next lower power of two below 'input' + */ + public static double floorPowerOfTwo(double d) { // Set fraction to "0". return Double.longBitsToDouble(Double.doubleToRawLongBits(d) & 0xFFF0_0000_0000_0000L); } - public static double[] normalizeCopy(double[] d) { + /** + * Calculates the {@link #floorPowerOfTwo(double)} of an array. + * @param d input vector + * @return copied vector with next lower power of two below 'input' + * @see #floorPowerOfTwo(double) + */ + public static double[] floorPowerOfTwoCopy(double[] d) { double[] d2 = new double[d.length]; for (int i = 0; i < d.length; i++) { - d2[i] = normalize(d[i]); + d2[i] = floorPowerOfTwo(d[i]); } - // System.out.println("NORM: " + Arrays.toString(d) + " -> " + Arrays.toString(d2)); return d2; } - } diff --git a/src/test/java/org/tinspin/index/test/PointMultimapTest.java b/src/test/java/org/tinspin/index/test/PointMultimapTest.java index f02e1e9..a6a7c6e 100644 --- a/src/test/java/org/tinspin/index/test/PointMultimapTest.java +++ b/src/test/java/org/tinspin/index/test/PointMultimapTest.java @@ -17,14 +17,11 @@ */ package org.tinspin.index.test; -import ch.ethz.globis.phtree.util.PhTreeStats; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.tinspin.index.Index; import org.tinspin.index.PointMultimap; -import org.tinspin.index.Stats; -import org.tinspin.index.qthypercube.QuadTreeKD; import org.tinspin.index.test.util.TestInstances; import java.util.*; @@ -141,29 +138,13 @@ private void smokeTest(List data) { int dim = data.get(0).p.length; PointMultimap tree = createTree(data.size(), dim); - int x = 0; for (Entry e : data) { - //System.out.println("x=" + x++); - x++; tree.insert(e.p, e); - try { - tree.getStats(); - } catch (RuntimeException e2) { - System.out.println("x=" + x++); - System.out.println(tree.toStringTree()); - throw e2; - } } // Check consistency - Stats s = tree.getStats(); - System.out.println(tree.getClass().getName() + " " + s.toString()); -// if (s instanceof QuadTreeKD.QStats) { -// System.out.println( " " + Arrays.toString(((QuadTreeKD.QStats)s).histoString()); -// } - //System.out.println(tree.toStringTree()); - - // System.out.println(tree.toStringTree()); + tree.getStats(); + for (Entry e : data) { PointIterator it = tree.queryExactPoint(e.p); assertTrue("query(point) failed: " + e, it.hasNext()); @@ -189,7 +170,6 @@ private void smokeTest(List data) { assertEquals(data.size(), nExtent); } - //System.out.println(tree.toStringTree()); tree.getStats(); for (Entry e : data) { diff --git a/src/test/java/org/tinspin/util/MathToolsTest.java b/src/test/java/org/tinspin/util/MathToolsTest.java new file mode 100644 index 0000000..161dc07 --- /dev/null +++ b/src/test/java/org/tinspin/util/MathToolsTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016-2024 Tilmann Zaeschke + * + * This file is part of TinSpin. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.tinspin.util; + +import static org.junit.Assert.*; +import org.junit.Test; +import org.tinspin.index.util.MathTools; + +import java.util.Arrays; + +public class MathToolsTest { + + @Test + public void powerOfTwoFloor() { + assertEquals(1./64., MathTools.floorPowerOfTwo(0.03), 0.0); + assertEquals(0.25, MathTools.floorPowerOfTwo(0.3), 0.0); + assertEquals(2, MathTools.floorPowerOfTwo(3), 0.0); + assertEquals(16, MathTools.floorPowerOfTwo(30), 0.0); + assertEquals(256, MathTools.floorPowerOfTwo(300), 0.0); + } + + @Test + public void powerOfTwoFloor_vector() { + double[] d = {0.03, 0.3, 3, 30, 300}; + double[] dCopy = MathTools.floorPowerOfTwoCopy(d); + assertFalse(Arrays.equals(d, dCopy)); + assertEquals(1./64., dCopy[0], 0.0); + assertEquals(0.25, dCopy[1], 0.0); + assertEquals(2, dCopy[2], 0.0); + assertEquals(16, dCopy[3], 0.0); + assertEquals(256, dCopy[4], 0.0); + } +} diff --git a/src/test/java/org/tinspin/MinHeapTest.java b/src/test/java/org/tinspin/util/MinHeapTest.java similarity index 99% rename from src/test/java/org/tinspin/MinHeapTest.java rename to src/test/java/org/tinspin/util/MinHeapTest.java index f729c0e..bd82bd9 100644 --- a/src/test/java/org/tinspin/MinHeapTest.java +++ b/src/test/java/org/tinspin/util/MinHeapTest.java @@ -14,7 +14,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */package org.tinspin; + */package org.tinspin.util; import org.junit.Test; import org.tinspin.index.util.MinHeapI; diff --git a/src/test/java/org/tinspin/MinMaxHeapTest.java b/src/test/java/org/tinspin/util/MinMaxHeapTest.java similarity index 99% rename from src/test/java/org/tinspin/MinMaxHeapTest.java rename to src/test/java/org/tinspin/util/MinMaxHeapTest.java index a8bc46d..4673d32 100644 --- a/src/test/java/org/tinspin/MinMaxHeapTest.java +++ b/src/test/java/org/tinspin/util/MinMaxHeapTest.java @@ -14,7 +14,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */package org.tinspin; + */package org.tinspin.util; import static org.junit.Assert.*; import org.junit.Test; From f55e4b80161bc79cbe80cd216c8de101391cd475 Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sun, 30 Jun 2024 13:22:28 +0200 Subject: [PATCH 10/12] Fix root-node precision problems --- .../tinspin/index/qthypercube2/QuadTreeKD2.java | 12 ++---------- .../java/org/tinspin/index/util/MathTools.java | 14 ++++++++++++++ src/test/java/org/tinspin/util/MathToolsTest.java | 8 ++++++++ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java b/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java index 43ec401..b9e63db 100644 --- a/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java +++ b/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java @@ -128,9 +128,9 @@ private void adjustRootSize(double[] key) { return; } if (root.getRadius() == INITIAL_RADIUS) { - double dMax = maxOrthoDistance(key, root.getCenter()); + double dMax = MathTools.maxDelta(key, root.getCenter()); for (int i = 0; i < root.getValueCount(); i++) { - dMax = Math.max(dMax, maxOrthoDistance(root.getValues()[i].point(), root.getCenter())); + dMax = Math.max(dMax, MathTools.maxDelta(root.getValues()[i].point(), root.getCenter())); } double radius = MathTools.floorPowerOfTwo(dMax) * 2; if (radius > 0) { @@ -142,14 +142,6 @@ private void adjustRootSize(double[] key) { } } - private static double maxOrthoDistance(double[] v1, double[] v2) { - double dMax = 0; - for (int i = 0; i < v1.length; i++) { - dMax = Math.max(dMax, Math.abs(v1[i] - v2[i])); - } - return dMax; - } - /** * Check whether a given key exists. * @param key the key to check diff --git a/src/main/java/org/tinspin/index/util/MathTools.java b/src/main/java/org/tinspin/index/util/MathTools.java index a866f7d..0a61ab0 100644 --- a/src/main/java/org/tinspin/index/util/MathTools.java +++ b/src/main/java/org/tinspin/index/util/MathTools.java @@ -45,4 +45,18 @@ public static double[] floorPowerOfTwoCopy(double[] d) { } return d2; } + + /** + * Returns the maximal delta between any pair of scalars in the vector. + * @param v1 vector 1 + * @param v2 vector 2 + * @return maximal delta (always >= 0). + */ + public static double maxDelta(double[] v1, double[] v2) { + double dMax = 0; + for (int i = 0; i < v1.length; i++) { + dMax = Math.max(dMax, Math.abs(v1[i] - v2[i])); + } + return dMax; + } } diff --git a/src/test/java/org/tinspin/util/MathToolsTest.java b/src/test/java/org/tinspin/util/MathToolsTest.java index 161dc07..3e1507a 100644 --- a/src/test/java/org/tinspin/util/MathToolsTest.java +++ b/src/test/java/org/tinspin/util/MathToolsTest.java @@ -45,4 +45,12 @@ public void powerOfTwoFloor_vector() { assertEquals(16, dCopy[3], 0.0); assertEquals(256, dCopy[4], 0.0); } + + @Test + public void maxDelta() { + assertEquals(12, MathTools.maxDelta(new double[]{-4.}, new double[]{8.}), 0.0); + assertEquals(12, MathTools.maxDelta(new double[]{8.}, new double[]{-4.}), 0.0); + + assertEquals(4, MathTools.maxDelta(new double[]{2, 4, 2}, new double[]{3, 8, 4}), 0.0); + } } From 97d1f454901115c3f625f4cafd8229fa6950218e Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sun, 30 Jun 2024 13:42:39 +0200 Subject: [PATCH 11/12] Fix root-node precision problems --- .../index/qthypercube2/QuadTreeKD2.java | 2 +- .../org/tinspin/index/util/MathTools.java | 16 +++++++-- .../java/org/tinspin/util/MathToolsTest.java | 35 +++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java b/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java index b9e63db..01d50d4 100644 --- a/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java +++ b/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java @@ -132,7 +132,7 @@ private void adjustRootSize(double[] key) { for (int i = 0; i < root.getValueCount(); i++) { dMax = Math.max(dMax, MathTools.maxDelta(root.getValues()[i].point(), root.getCenter())); } - double radius = MathTools.floorPowerOfTwo(dMax) * 2; + double radius = MathTools.ceilPowerOfTwo(dMax + QUtil.EPS_MUL); if (radius > 0) { root.adjustRadius(radius); } else if (root.getValueCount() >= maxNodeSize - 1) { diff --git a/src/main/java/org/tinspin/index/util/MathTools.java b/src/main/java/org/tinspin/index/util/MathTools.java index 0a61ab0..9fe89dd 100644 --- a/src/main/java/org/tinspin/index/util/MathTools.java +++ b/src/main/java/org/tinspin/index/util/MathTools.java @@ -21,11 +21,23 @@ public class MathTools { private MathTools() {} + /** + * Similar to Math.ceil() with the ceiling being the next higher power of 2. + * The resulting number can repeatedly and (almost) always be divided by two without loss of precision. + * @param d input + * @return next power of two above or equal to 'input' + */ + public static double ceilPowerOfTwo(double d) { + double ceil = floorPowerOfTwo(d); + return ceil == d ? ceil : ceil * 2; + } + /** * Similar to Math.floor() with the floor being the next lower power of 2. - * The resulting number can repeatedly (almost) always be divided by two without loss of precision. + * The resulting number can repeatedly and (almost) always be divided by two without loss of precision. + * We calculate the "floor" by setting the "fraction" of the bit representation to 0. * @param d input - * @return next lower power of two below 'input' + * @return next power of two below or equal to 'input' */ public static double floorPowerOfTwo(double d) { // Set fraction to "0". diff --git a/src/test/java/org/tinspin/util/MathToolsTest.java b/src/test/java/org/tinspin/util/MathToolsTest.java index 3e1507a..c33150f 100644 --- a/src/test/java/org/tinspin/util/MathToolsTest.java +++ b/src/test/java/org/tinspin/util/MathToolsTest.java @@ -25,6 +25,28 @@ public class MathToolsTest { + @Test + public void powerOfTwoCeil() { + assertEquals(1./32., MathTools.ceilPowerOfTwo(0.03), 0.0); + assertEquals(0.5, MathTools.ceilPowerOfTwo(0.3), 0.0); + assertEquals(4, MathTools.ceilPowerOfTwo(3), 0.0); + assertEquals(32, MathTools.ceilPowerOfTwo(30), 0.0); + assertEquals(512, MathTools.ceilPowerOfTwo(300), 0.0); + + assertEquals(-0.5, MathTools.ceilPowerOfTwo(-0.3), 0.0); + assertEquals(-4, MathTools.ceilPowerOfTwo(-3), 0.0); + assertEquals(-32, MathTools.ceilPowerOfTwo(-30), 0.0); + + // identity + assertEquals(0, MathTools.ceilPowerOfTwo(0), 0.0); + assertEquals(-0.5, MathTools.ceilPowerOfTwo(-0.5), 0.0); + assertEquals(0.5, MathTools.ceilPowerOfTwo(0.5), 0.0); + assertEquals(-1, MathTools.ceilPowerOfTwo(-1), 0.0); + assertEquals(1, MathTools.ceilPowerOfTwo(1), 0.0); + assertEquals(-2, MathTools.ceilPowerOfTwo(-2), 0.0); + assertEquals(2, MathTools.ceilPowerOfTwo(2), 0.0); + } + @Test public void powerOfTwoFloor() { assertEquals(1./64., MathTools.floorPowerOfTwo(0.03), 0.0); @@ -32,6 +54,19 @@ public void powerOfTwoFloor() { assertEquals(2, MathTools.floorPowerOfTwo(3), 0.0); assertEquals(16, MathTools.floorPowerOfTwo(30), 0.0); assertEquals(256, MathTools.floorPowerOfTwo(300), 0.0); + + assertEquals(-0.25, MathTools.floorPowerOfTwo(-0.3), 0.0); + assertEquals(-2, MathTools.floorPowerOfTwo(-3), 0.0); + assertEquals(-16, MathTools.floorPowerOfTwo(-30), 0.0); + + // identity + assertEquals(0, MathTools.ceilPowerOfTwo(0), 0.0); + assertEquals(-0.5, MathTools.ceilPowerOfTwo(-0.5), 0.0); + assertEquals(0.5, MathTools.ceilPowerOfTwo(0.5), 0.0); + assertEquals(-1, MathTools.ceilPowerOfTwo(-1), 0.0); + assertEquals(1, MathTools.ceilPowerOfTwo(1), 0.0); + assertEquals(-2, MathTools.ceilPowerOfTwo(-2), 0.0); + assertEquals(2, MathTools.ceilPowerOfTwo(2), 0.0); } @Test From 538302e889c41828ab3b72314abac0b252af1c27 Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sun, 30 Jun 2024 14:04:09 +0200 Subject: [PATCH 12/12] Fix root-node precision problems --- src/main/java/org/tinspin/index/util/MathTools.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/tinspin/index/util/MathTools.java b/src/main/java/org/tinspin/index/util/MathTools.java index 9fe89dd..2507478 100644 --- a/src/main/java/org/tinspin/index/util/MathTools.java +++ b/src/main/java/org/tinspin/index/util/MathTools.java @@ -62,7 +62,7 @@ public static double[] floorPowerOfTwoCopy(double[] d) { * Returns the maximal delta between any pair of scalars in the vector. * @param v1 vector 1 * @param v2 vector 2 - * @return maximal delta (always >= 0). + * @return maximal delta (positive or zero). */ public static double maxDelta(double[] v1, double[] v2) { double dMax = 0;