From d5c865bb633474290aa6d20ffe32c467fd5fa9a6 Mon Sep 17 00:00:00 2001 From: Titan Yuan Date: Fri, 3 Jan 2025 15:42:58 -0800 Subject: [PATCH 01/13] Add cluster class --- Assets/Scripts/Algorithms.meta | 8 ++ Assets/Scripts/Algorithms/Cluster.cs | 75 ++++++++++++++++++ Assets/Scripts/Algorithms/Cluster.cs.meta | 2 + Assets/Tests/EditMode/ClusterTest.cs | 95 +++++++++++++++++++++++ Assets/Tests/EditMode/ClusterTest.cs.meta | 2 + 5 files changed, 182 insertions(+) create mode 100644 Assets/Scripts/Algorithms.meta create mode 100644 Assets/Scripts/Algorithms/Cluster.cs create mode 100644 Assets/Scripts/Algorithms/Cluster.cs.meta create mode 100644 Assets/Tests/EditMode/ClusterTest.cs create mode 100644 Assets/Tests/EditMode/ClusterTest.cs.meta diff --git a/Assets/Scripts/Algorithms.meta b/Assets/Scripts/Algorithms.meta new file mode 100644 index 00000000..125d5297 --- /dev/null +++ b/Assets/Scripts/Algorithms.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5ce134601efc54aa28f0284924ffbd01 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Algorithms/Cluster.cs b/Assets/Scripts/Algorithms/Cluster.cs new file mode 100644 index 00000000..1df4d8e2 --- /dev/null +++ b/Assets/Scripts/Algorithms/Cluster.cs @@ -0,0 +1,75 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +// The cluster class represents a collection of points with a defined centroid. +public class Cluster { + // Centroid of the cluster. + private Vector3 centroid = Vector3.zero; + + // List of points in the cluster. + private List points = new List(); + + // Get the list of points. + public IReadOnlyList Points { + get { return points; } + } + + // Return the size of the cluster. + public int Size() { + return points.Count; + } + + // Check whether the cluster is empty. + public bool IsEmpty() { + return Size() == 0; + } + + // Calculate the radius of the cluster. + public float Radius() { + if (IsEmpty()) { + return 0; + } + + Vector3 centroid = Centroid(); + return points.Max(point => Vector3.Distance(centroid, point)); + } + + // Calculate the centroid of the cluster. + public Vector3 Centroid() { + if (IsEmpty()) { + return Vector3.zero; + } + + Vector3 centroid = Vector3.zero; + foreach (var point in points) { + centroid += point; + } + centroid /= points.Count; + return centroid; + } + + // Recenter the cluster's centroid to be the mean of all points in the cluster. + public void Recenter() { + centroid = Centroid(); + } + + // Add a point to the cluster. + // This function does not update the centroid of the cluster. + public void AddPoint(in Vector3 point) { + points.Add(point); + } + + // Add multiple points to the cluster. + // This function does not update the centroid of the cluster. + public void AddPoints(in IReadOnlyList otherPoints) { + points.AddRange(otherPoints); + } + + // Merge another cluster into this one. + // This function does not update the centroid of the cluster. + public void Merge(in Cluster cluster) { + AddPoints(cluster.Points); + } +} diff --git a/Assets/Scripts/Algorithms/Cluster.cs.meta b/Assets/Scripts/Algorithms/Cluster.cs.meta new file mode 100644 index 00000000..145d5082 --- /dev/null +++ b/Assets/Scripts/Algorithms/Cluster.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6466e5b76f1704b55ae0ea8493b999a0 \ No newline at end of file diff --git a/Assets/Tests/EditMode/ClusterTest.cs b/Assets/Tests/EditMode/ClusterTest.cs new file mode 100644 index 00000000..7bc520af --- /dev/null +++ b/Assets/Tests/EditMode/ClusterTest.cs @@ -0,0 +1,95 @@ +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using System.Collections; +using System.Collections.Generic; + +public class ClusterTest { + public static Cluster GenerateCluster(in IReadOnlyList points) { + Cluster cluster = new Cluster(); + cluster.AddPoints(points); + cluster.Recenter(); + return cluster; + } + + [Test] + public void TestSize() { + const int size = 10; + List points = new List(); + for (int i = 0; i < size; ++i) { + points.Add(new Vector3(0, i, 0)); + } + Cluster cluster = GenerateCluster(points); + Assert.AreEqual(cluster.Size(), size); + } + + [Test] + public void TestIsEmpty() { + Cluster emptyCluster = new Cluster(); + Assert.IsTrue(emptyCluster.IsEmpty()); + + Cluster cluster = new Cluster(); + cluster.AddPoint(new Vector3(1, -1, 0)); + Assert.IsFalse(cluster.IsEmpty()); + } + + [Test] + public void TestRadius() { + const float radius = 5; + List points = new List { + new Vector3(0, radius, 0), + new Vector3(0, -radius, 0), + }; + Cluster cluster = GenerateCluster(points); + Assert.AreEqual(cluster.Radius(), radius); + } + + [Test] + public void TestCentroid() { + const float radius = 3; + List points = new List(); + for (int i = -1; i <= 1; ++i) { + for (int j = -1; j <= 1; ++j) { + points.Add(new Vector3(i, j, 0)); + } + } + Cluster cluster = GenerateCluster(points); + Assert.AreEqual(cluster.Centroid(), Vector3.zero); + } + + [Test] + public void TestRecenter() { + const float radius = 3; + List points = new List(); + for (int i = -1; i <= 1; ++i) { + for (int j = -1; j <= 1; ++j) { + points.Add(new Vector3(i, j, 0)); + } + } + Cluster cluster = GenerateCluster(points); + cluster.AddPoint(new Vector3(10, -10, 0)); + cluster.Recenter(); + Assert.AreEqual(cluster.Centroid(), new Vector3(1, -1, 0)); + } + + [Test] + public void TestMerge() { + const int size = 10; + List points1 = new List(); + List points2 = new List(); + for (int i = 0; i < size; ++i) { + points1.Add(new Vector3(0, i, 0)); + points2.Add(new Vector3(i, 0, 0)); + } + Cluster cluster1 = GenerateCluster(points1); + Cluster cluster2 = GenerateCluster(points2); + int size1 = cluster1.Size(); + int size2 = cluster2.Size(); + Vector3 centroid1 = cluster1.Centroid(); + Vector3 centroid2 = cluster2.Centroid(); + cluster1.Merge(cluster2); + cluster1.Recenter(); + Assert.AreEqual(cluster1.Size(), size1 + size2); + Assert.AreEqual(cluster1.Centroid(), (centroid1 + centroid2) / 2); + } +} diff --git a/Assets/Tests/EditMode/ClusterTest.cs.meta b/Assets/Tests/EditMode/ClusterTest.cs.meta new file mode 100644 index 00000000..bb4c2b32 --- /dev/null +++ b/Assets/Tests/EditMode/ClusterTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 74be2838017f7416283597f9f3275791 \ No newline at end of file From 09b6c94fd18761d3a7a90482eab63fbe50ed49a3 Mon Sep 17 00:00:00 2001 From: Titan Yuan Date: Fri, 3 Jan 2025 17:46:08 -0800 Subject: [PATCH 02/13] Add k-means clustering --- Assets/Scripts/Algorithms/Cluster.cs | 16 +++- Assets/Scripts/Algorithms/Clusterer.cs | 29 +++++++ Assets/Scripts/Algorithms/Clusterer.cs.meta | 2 + Assets/Scripts/Algorithms/KMeansClusterer.cs | 79 +++++++++++++++++++ .../Algorithms/KMeansClusterer.cs.meta | 2 + Assets/Tests/EditMode/ClusterTest.cs | 5 +- Assets/Tests/EditMode/KMeansClustererTest.cs | 24 ++++++ .../EditMode/KMeansClustererTest.cs.meta | 2 + 8 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 Assets/Scripts/Algorithms/Clusterer.cs create mode 100644 Assets/Scripts/Algorithms/Clusterer.cs.meta create mode 100644 Assets/Scripts/Algorithms/KMeansClusterer.cs create mode 100644 Assets/Scripts/Algorithms/KMeansClusterer.cs.meta create mode 100644 Assets/Tests/EditMode/KMeansClustererTest.cs create mode 100644 Assets/Tests/EditMode/KMeansClustererTest.cs.meta diff --git a/Assets/Scripts/Algorithms/Cluster.cs b/Assets/Scripts/Algorithms/Cluster.cs index 1df4d8e2..f7a5b39c 100644 --- a/Assets/Scripts/Algorithms/Cluster.cs +++ b/Assets/Scripts/Algorithms/Cluster.cs @@ -5,12 +5,22 @@ // The cluster class represents a collection of points with a defined centroid. public class Cluster { - // Centroid of the cluster. - private Vector3 centroid = Vector3.zero; + // Position of the cluster. + private Vector3 position = Vector3.zero; // List of points in the cluster. private List points = new List(); + public Cluster() {} + public Cluster(in Vector3 position) { + this.position = position; + } + + // Get the cluster position. + public Vector3 Position { + get { return position; } + } + // Get the list of points. public IReadOnlyList Points { get { return points; } @@ -52,7 +62,7 @@ public Vector3 Centroid() { // Recenter the cluster's centroid to be the mean of all points in the cluster. public void Recenter() { - centroid = Centroid(); + position = Centroid(); } // Add a point to the cluster. diff --git a/Assets/Scripts/Algorithms/Clusterer.cs b/Assets/Scripts/Algorithms/Clusterer.cs new file mode 100644 index 00000000..a5c805a4 --- /dev/null +++ b/Assets/Scripts/Algorithms/Clusterer.cs @@ -0,0 +1,29 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +// The clusterer class is an interface for clustering algorithms. +public abstract class IClusterer { + // List of points to cluster. + protected List points = new List(); + + // List of clusters. + protected List clusters = new List(); + + public IClusterer(List points) { + this.points = points; + } + + // Get the list of points. + public IReadOnlyList Points { + get { return points; } + } + + // Get the list of clusters. + public IReadOnlyList Clusters { + get { return clusters; } + } + + // Cluster the points. + public abstract void Cluster(); +} diff --git a/Assets/Scripts/Algorithms/Clusterer.cs.meta b/Assets/Scripts/Algorithms/Clusterer.cs.meta new file mode 100644 index 00000000..19e3d02b --- /dev/null +++ b/Assets/Scripts/Algorithms/Clusterer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 91be483c8155f45688387ac68e79046f \ No newline at end of file diff --git a/Assets/Scripts/Algorithms/KMeansClusterer.cs b/Assets/Scripts/Algorithms/KMeansClusterer.cs new file mode 100644 index 00000000..acec318d --- /dev/null +++ b/Assets/Scripts/Algorithms/KMeansClusterer.cs @@ -0,0 +1,79 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using System; + +// The k-means clusterer class performs k-means clustering. +public class KMeansClusterer : IClusterer { + public const float Epsilon = 1e-3f; + + // Number of clusters. + private int k = 0; + + // Maximum number of iterations. + private int maxIterations = 20; + + public KMeansClusterer(List points, int k, int maxIterations = 20) : base(points) { + this.k = k; + this.maxIterations = maxIterations; + } + + // Cluster the points. + public override void Cluster() { + // Initialize the clusters with centroids at random points. + // Perform Fisher-Yates shuffling to find k random points. + System.Random random = new System.Random(); + for (int i = points.Count - 1; i >= points.Count - k; --i) { + int j = random.Next(i + 1); + (points[i], points[j]) = (points[j], points[i]); + } + for (int i = points.Count - 1; i >= points.Count - k; --i) { + clusters.Add(new Cluster(points[i])); + } + + bool converged = false; + int iteration = 0; + while (!converged && iteration < maxIterations) { + AssignPointsToCluster(); + + // Calculate the new clusters as the mean of all assigned points. + converged = true; + for (int clusterIndex = 0; clusterIndex < clusters.Count; ++clusterIndex) { + Cluster newCluster; + if (clusters[clusterIndex].IsEmpty()) { + int pointIndex = random.Next(points.Count); + newCluster = new Cluster(points[pointIndex]); + } else { + newCluster = new Cluster(clusters[clusterIndex].Centroid()); + } + + // Check whether the algorithm has converged by checking whether the cluster has moved. + if (Vector3.Distance(newCluster.Position, clusters[clusterIndex].Position) > Epsilon) { + converged = false; + } + + clusters[clusterIndex] = newCluster; + } + + ++iteration; + } + + AssignPointsToCluster(); + } + + private void AssignPointsToCluster() { + // Determine the closest centroid to each point. + foreach (var point in points) { + float minDistance = Mathf.Infinity; + int minIndex = -1; + for (int clusterIndex = 0; clusterIndex < clusters.Count; ++clusterIndex) { + float distance = Vector3.Distance(clusters[clusterIndex].Position, point); + if (distance < minDistance) { + minDistance = distance; + minIndex = clusterIndex; + } + } + clusters[minIndex].AddPoint(point); + } + } +} diff --git a/Assets/Scripts/Algorithms/KMeansClusterer.cs.meta b/Assets/Scripts/Algorithms/KMeansClusterer.cs.meta new file mode 100644 index 00000000..e0f9abb8 --- /dev/null +++ b/Assets/Scripts/Algorithms/KMeansClusterer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ebb68733cf6dd4516848a1b7f5ff11e3 \ No newline at end of file diff --git a/Assets/Tests/EditMode/ClusterTest.cs b/Assets/Tests/EditMode/ClusterTest.cs index 7bc520af..03fadc48 100644 --- a/Assets/Tests/EditMode/ClusterTest.cs +++ b/Assets/Tests/EditMode/ClusterTest.cs @@ -68,8 +68,9 @@ public void TestRecenter() { } Cluster cluster = GenerateCluster(points); cluster.AddPoint(new Vector3(10, -10, 0)); + Assert.AreNotEqual(cluster.Position, new Vector3(1, -1, 0)); cluster.Recenter(); - Assert.AreEqual(cluster.Centroid(), new Vector3(1, -1, 0)); + Assert.AreEqual(cluster.Position, new Vector3(1, -1, 0)); } [Test] @@ -90,6 +91,6 @@ public void TestMerge() { cluster1.Merge(cluster2); cluster1.Recenter(); Assert.AreEqual(cluster1.Size(), size1 + size2); - Assert.AreEqual(cluster1.Centroid(), (centroid1 + centroid2) / 2); + Assert.AreEqual(cluster1.Position, (centroid1 + centroid2) / 2); } } diff --git a/Assets/Tests/EditMode/KMeansClustererTest.cs b/Assets/Tests/EditMode/KMeansClustererTest.cs new file mode 100644 index 00000000..e3cc66c3 --- /dev/null +++ b/Assets/Tests/EditMode/KMeansClustererTest.cs @@ -0,0 +1,24 @@ +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using System.Collections; +using System.Collections.Generic; + +public class KMeansClustererTest { + public static readonly List Points = new List { + new Vector3(0, 0, 0), + new Vector3(0, 1, 0), + new Vector3(0, 1.5f, 0), + new Vector3(0, 2.5f, 0), + }; + + [Test] + public void TestSingleCluster() { + KMeansClusterer clusterer = new KMeansClusterer(Points, k: 1); + clusterer.Cluster(); + Cluster cluster = clusterer.Clusters[0]; + Assert.AreEqual(cluster.Size(), Points.Count); + Assert.AreEqual(cluster.Position, new Vector3(0, 1.25f, 0)); + Assert.AreEqual(cluster.Centroid(), new Vector3(0, 1.25f, 0)); + } +} diff --git a/Assets/Tests/EditMode/KMeansClustererTest.cs.meta b/Assets/Tests/EditMode/KMeansClustererTest.cs.meta new file mode 100644 index 00000000..77103751 --- /dev/null +++ b/Assets/Tests/EditMode/KMeansClustererTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0380bb64b0b224eb2a89bd5bfec704d3 \ No newline at end of file From 3c02ed47c9ac526eddd7f5b60bc1dc143ded05ae Mon Sep 17 00:00:00 2001 From: Titan Yuan Date: Sat, 4 Jan 2025 01:33:19 -0800 Subject: [PATCH 03/13] Add constrained k-means clustering --- Assets/Scripts/Algorithms/Clusterer.cs | 18 +++++++ Assets/Scripts/Algorithms/KMeansClusterer.cs | 37 +++++++++++++++ Assets/Tests/EditMode/KMeansClustererTest.cs | 50 ++++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/Assets/Scripts/Algorithms/Clusterer.cs b/Assets/Scripts/Algorithms/Clusterer.cs index a5c805a4..eb5295dd 100644 --- a/Assets/Scripts/Algorithms/Clusterer.cs +++ b/Assets/Scripts/Algorithms/Clusterer.cs @@ -27,3 +27,21 @@ public IReadOnlyList Clusters { // Cluster the points. public abstract void Cluster(); } + +// The size and radius-constrained clusterer class is an interface for clustering algorithms with +// size and radius constraints. The size is defined as the maximum number of points within a +// cluster, and the radius denotes the maximum distance from the cluster's centroid to any of its +// assigned points. +public abstract class ISizeAndRadiusConstrainedClusterer : IClusterer { + // Maximum cluster size. + protected readonly int maxSize = 0; + + // Maximum cluster radius. + protected readonly float maxRadius = 0; + + public ISizeAndRadiusConstrainedClusterer(List points, int maxSize, float maxRadius) + : base(points) { + this.maxSize = maxSize; + this.maxRadius = maxRadius; + } +} diff --git a/Assets/Scripts/Algorithms/KMeansClusterer.cs b/Assets/Scripts/Algorithms/KMeansClusterer.cs index acec318d..b32a2c9d 100644 --- a/Assets/Scripts/Algorithms/KMeansClusterer.cs +++ b/Assets/Scripts/Algorithms/KMeansClusterer.cs @@ -77,3 +77,40 @@ private void AssignPointsToCluster() { } } } + +// The k-means clusterer class performs k-means clustering. +public class ConstrainedKMeansClusterer : ISizeAndRadiusConstrainedClusterer { + public ConstrainedKMeansClusterer(List points, int maxSize, float maxRadius) + : base(points, maxSize, maxRadius) {} + + // Cluster the points. + public override void Cluster() { + int numClusters = (int)Mathf.Ceil(points.Count / maxSize); + KMeansClusterer clusterer; + while (true) { + clusterer = new KMeansClusterer(points, numClusters); + clusterer.Cluster(); + + // Count the number of over-populated and over-sized clusters. + int numOverPopulatedClusters = 0; + int numOverSizedClusters = 0; + foreach (var cluster in clusterer.Clusters) { + if (cluster.Size() > maxSize) { + ++numOverPopulatedClusters; + } + if (cluster.Radius() > maxRadius) { + ++numOverSizedClusters; + } + } + + // If all clusters satisfy the size and radius constraints, the algorithm has converged. + if (numOverPopulatedClusters == 0 && numOverSizedClusters == 0) { + break; + } + + numClusters += + (int)Mathf.Ceil(Mathf.Max(numOverPopulatedClusters, numOverSizedClusters) / 2.0f); + } + clusters = new List(clusterer.Clusters); + } +} diff --git a/Assets/Tests/EditMode/KMeansClustererTest.cs b/Assets/Tests/EditMode/KMeansClustererTest.cs index e3cc66c3..7e0daee5 100644 --- a/Assets/Tests/EditMode/KMeansClustererTest.cs +++ b/Assets/Tests/EditMode/KMeansClustererTest.cs @@ -22,3 +22,53 @@ public void TestSingleCluster() { Assert.AreEqual(cluster.Centroid(), new Vector3(0, 1.25f, 0)); } } + +public class ConstrainedKMeansClustererTest { + public static readonly List Points = new List { + new Vector3(0, 0, 0), + new Vector3(0, 1, 0), + new Vector3(0, 1.5f, 0), + new Vector3(0, 2.5f, 0), + }; + + [Test] + public void TestSingleCluster() { + ConstrainedKMeansClusterer clusterer = + new ConstrainedKMeansClusterer(Points, maxSize: Points.Count, maxRadius: Mathf.Infinity); + clusterer.Cluster(); + Assert.AreEqual(clusterer.Clusters.Count, 1); + Cluster cluster = clusterer.Clusters[0]; + Assert.AreEqual(cluster.Size(), Points.Count); + Assert.AreEqual(cluster.Centroid(), new Vector3(0, 1.25f, 0)); + } + + [Test] + public void TestMaxSizeOne() { + ConstrainedKMeansClusterer clusterer = + new ConstrainedKMeansClusterer(Points, maxSize: 1, maxRadius: Mathf.Infinity); + clusterer.Cluster(); + Assert.AreEqual(clusterer.Clusters.Count, Points.Count); + foreach (var cluster in clusterer.Clusters) { + Assert.AreEqual(cluster.Size(), 1); + } + } + + [Test] + public void TestZeroRadius() { + ConstrainedKMeansClusterer clusterer = + new ConstrainedKMeansClusterer(Points, maxSize: Points.Count, maxRadius: 0); + clusterer.Cluster(); + Assert.AreEqual(clusterer.Clusters.Count, Points.Count); + foreach (var cluster in clusterer.Clusters) { + Assert.AreEqual(cluster.Size(), 1); + } + } + + [Test] + public void TestSmallRadius() { + ConstrainedKMeansClusterer clusterer = + new ConstrainedKMeansClusterer(Points, maxSize: Points.Count, maxRadius: 1); + clusterer.Cluster(); + Assert.AreEqual(clusterer.Clusters.Count, 2); + } +} From a84b2c800a9078025e2d2139ebace6cb43f631f0 Mon Sep 17 00:00:00 2001 From: Titan Yuan Date: Sat, 4 Jan 2025 21:12:09 -0800 Subject: [PATCH 04/13] Add constrained agglomerative clustering --- .../Algorithms/AgglomerativeClusterer.cs | 100 ++++++++++++++++++ .../Algorithms/AgglomerativeClusterer.cs.meta | 2 + Assets/Scripts/Algorithms/KMeansClusterer.cs | 3 +- .../EditMode/AgglomerativeClustererTest.cs | 63 +++++++++++ .../AgglomerativeClustererTest.cs.meta | 2 + Assets/Tests/EditMode/KMeansClustererTest.cs | 6 ++ 6 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 Assets/Scripts/Algorithms/AgglomerativeClusterer.cs create mode 100644 Assets/Scripts/Algorithms/AgglomerativeClusterer.cs.meta create mode 100644 Assets/Tests/EditMode/AgglomerativeClustererTest.cs create mode 100644 Assets/Tests/EditMode/AgglomerativeClustererTest.cs.meta diff --git a/Assets/Scripts/Algorithms/AgglomerativeClusterer.cs b/Assets/Scripts/Algorithms/AgglomerativeClusterer.cs new file mode 100644 index 00000000..0a9c4578 --- /dev/null +++ b/Assets/Scripts/Algorithms/AgglomerativeClusterer.cs @@ -0,0 +1,100 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using System; + +// The agglomerative clusterer class performs agglomerative clustering with the stopping condition +// given by the size and radius constraints. +public class AgglomerativeClusterer : ISizeAndRadiusConstrainedClusterer { + public AgglomerativeClusterer(List points, int maxSize, float maxRadius) + : base(points, maxSize, maxRadius) {} + + // Cluster the points. + public override void Cluster() { + // Add a cluster for every point. + foreach (var point in points) { + Cluster cluster = new Cluster(point); + cluster.AddPoint(point); + clusters.Add(cluster); + } + + // Create a set containing all valid cluster indices. + HashSet validClusterIndices = new HashSet(); + for (int i = 0; i < clusters.Count; ++i) { + validClusterIndices.Add(i); + } + + // Find the pairwise distances between all clusters. + // The upper triangular half of the distances matrix is unused. + float[,] distances = new float[clusters.Count, clusters.Count]; + for (int i = 0; i < clusters.Count; ++i) { + for (int j = 0; j < i; ++j) { + distances[i, j] = Vector3.Distance(clusters[i].Position, clusters[j].Position); + } + } + + while (true) { + // Find the minimum distance between two clusters. + float minDistance = Mathf.Infinity; + int clusterIndex1 = -1; + int clusterIndex2 = -1; + for (int i = 0; i < clusters.Count; ++i) { + for (int j = 0; j < i; ++j) { + if (distances[i, j] < minDistance) { + minDistance = distances[i, j]; + clusterIndex1 = i; + clusterIndex2 = j; + } + } + } + + // Check whether the minimum distance exceeds the maximum cluster radius, in which case the + // algorithm has converged. This produces a conservative solution because the radius of a + // merged cluster is less than or equal to the sum of the original cluster radii. + if (minDistance >= maxRadius) { + break; + } + + // Check whether merging the two clusters would violate the size constraint. + if (clusters[clusterIndex1].Size() + clusters[clusterIndex2].Size() > maxSize) { + distances[clusterIndex1, clusterIndex2] = Mathf.Infinity; + continue; + } + + // Merge the two clusters together. + int minClusterIndex = Mathf.Min(clusterIndex1, clusterIndex2); + int maxClusterIndex = Mathf.Max(clusterIndex1, clusterIndex2); + clusters[minClusterIndex].Merge(clusters[maxClusterIndex]); + clusters[minClusterIndex].Recenter(); + validClusterIndices.Remove(maxClusterIndex); + + // Update the distances matrix using the distance between the cluster centroids. + // TODO(titan): Change the distance metric to use average or maximum linkage. + for (int i = 0; i < minClusterIndex; ++i) { + if (distances[minClusterIndex, i] < Mathf.Infinity) { + distances[minClusterIndex, i] = + Vector3.Distance(clusters[minClusterIndex].Position, clusters[i].Position); + } + } + for (int i = minClusterIndex + 1; i < clusters.Count; ++i) { + if (distances[i, minClusterIndex] < Mathf.Infinity) { + distances[i, minClusterIndex] = + Vector3.Distance(clusters[minClusterIndex].Position, clusters[i].Position); + } + } + for (int i = 0; i < maxClusterIndex; ++i) { + distances[maxClusterIndex, i] = Mathf.Infinity; + } + for (int i = maxClusterIndex + 1; i < clusters.Count; ++i) { + distances[i, maxClusterIndex] = Mathf.Infinity; + } + } + + // Select only the valid clusters. + for (int i = clusters.Count - 1; i >= 0; --i) { + if (!validClusterIndices.Contains(i)) { + clusters.RemoveAt(i); + } + } + } +} diff --git a/Assets/Scripts/Algorithms/AgglomerativeClusterer.cs.meta b/Assets/Scripts/Algorithms/AgglomerativeClusterer.cs.meta new file mode 100644 index 00000000..e9660687 --- /dev/null +++ b/Assets/Scripts/Algorithms/AgglomerativeClusterer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 847699c106b964d89b4fa0cbc78e6694 \ No newline at end of file diff --git a/Assets/Scripts/Algorithms/KMeansClusterer.cs b/Assets/Scripts/Algorithms/KMeansClusterer.cs index b32a2c9d..b38eb991 100644 --- a/Assets/Scripts/Algorithms/KMeansClusterer.cs +++ b/Assets/Scripts/Algorithms/KMeansClusterer.cs @@ -78,7 +78,8 @@ private void AssignPointsToCluster() { } } -// The k-means clusterer class performs k-means clustering. +// The constrained k-means clusterer class performs k-means clustering under size and radius +// constraints. public class ConstrainedKMeansClusterer : ISizeAndRadiusConstrainedClusterer { public ConstrainedKMeansClusterer(List points, int maxSize, float maxRadius) : base(points, maxSize, maxRadius) {} diff --git a/Assets/Tests/EditMode/AgglomerativeClustererTest.cs b/Assets/Tests/EditMode/AgglomerativeClustererTest.cs new file mode 100644 index 00000000..e294da77 --- /dev/null +++ b/Assets/Tests/EditMode/AgglomerativeClustererTest.cs @@ -0,0 +1,63 @@ +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +public class AgglomerativeClustererTest { + public static readonly List Points = new List { + new Vector3(0, 0, 0), + new Vector3(0, 1, 0), + new Vector3(0, 1.5f, 0), + new Vector3(0, 2.5f, 0), + }; + + [Test] + public void TestSingleCluster() { + AgglomerativeClusterer clusterer = + new AgglomerativeClusterer(Points, maxSize: Points.Count, maxRadius: Mathf.Infinity); + clusterer.Cluster(); + Assert.AreEqual(clusterer.Clusters.Count, 1); + Cluster cluster = clusterer.Clusters[0]; + Assert.AreEqual(cluster.Size(), Points.Count); + Assert.AreEqual(cluster.Centroid(), new Vector3(0, 1.25f, 0)); + } + + [Test] + public void TestMaxSizeOne() { + AgglomerativeClusterer clusterer = + new AgglomerativeClusterer(Points, maxSize: 1, maxRadius: Mathf.Infinity); + clusterer.Cluster(); + Assert.AreEqual(clusterer.Clusters.Count, Points.Count); + foreach (var cluster in clusterer.Clusters) { + Assert.AreEqual(cluster.Size(), 1); + } + } + + [Test] + public void TestZeroRadius() { + AgglomerativeClusterer clusterer = + new AgglomerativeClusterer(Points, maxSize: Points.Count, maxRadius: 0); + clusterer.Cluster(); + Assert.AreEqual(clusterer.Clusters.Count, Points.Count); + foreach (var cluster in clusterer.Clusters) { + Assert.AreEqual(cluster.Size(), 1); + } + } + + [Test] + public void TestSmallRadius() { + AgglomerativeClusterer clusterer = + new AgglomerativeClusterer(Points, maxSize: Points.Count, maxRadius: 1); + clusterer.Cluster(); + Assert.AreEqual(clusterer.Clusters.Count, 3); + List clusters = clusterer.Clusters.OrderBy(cluster => cluster.Position[1]).ToList(); + Assert.AreEqual(clusters[0].Size(), 1); + Assert.AreEqual(clusters[0].Position, new Vector3(0, 0, 0)); + Assert.AreEqual(clusters[1].Size(), 2); + Assert.AreEqual(clusters[1].Position, new Vector3(0, 1.25f, 0)); + Assert.AreEqual(clusters[2].Size(), 1); + Assert.AreEqual(clusters[2].Position, new Vector3(0, 2.5f, 0)); + } +} diff --git a/Assets/Tests/EditMode/AgglomerativeClustererTest.cs.meta b/Assets/Tests/EditMode/AgglomerativeClustererTest.cs.meta new file mode 100644 index 00000000..a1254489 --- /dev/null +++ b/Assets/Tests/EditMode/AgglomerativeClustererTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 04f19bc2ee71841a8bf1d11791707c9b \ No newline at end of file diff --git a/Assets/Tests/EditMode/KMeansClustererTest.cs b/Assets/Tests/EditMode/KMeansClustererTest.cs index 7e0daee5..259fd796 100644 --- a/Assets/Tests/EditMode/KMeansClustererTest.cs +++ b/Assets/Tests/EditMode/KMeansClustererTest.cs @@ -3,6 +3,7 @@ using UnityEngine.TestTools; using System.Collections; using System.Collections.Generic; +using System.Linq; public class KMeansClustererTest { public static readonly List Points = new List { @@ -70,5 +71,10 @@ public void TestSmallRadius() { new ConstrainedKMeansClusterer(Points, maxSize: Points.Count, maxRadius: 1); clusterer.Cluster(); Assert.AreEqual(clusterer.Clusters.Count, 2); + List clusters = clusterer.Clusters.OrderBy(cluster => cluster.Position[1]).ToList(); + Assert.AreEqual(clusters[0].Size(), 2); + Assert.AreEqual(clusters[0].Position, new Vector3(0, 0.5f, 0)); + Assert.AreEqual(clusters[1].Size(), 2); + Assert.AreEqual(clusters[1].Position, new Vector3(0, 2, 0)); } } From 7d1107b55ac30f46673c2feb0978ec3c7cfacec2 Mon Sep 17 00:00:00 2001 From: Titan Yuan Date: Sat, 4 Jan 2025 21:18:29 -0800 Subject: [PATCH 05/13] Fix order of imports --- Assets/Scripts/Algorithms/AgglomerativeClusterer.cs | 1 - Assets/Scripts/Algorithms/KMeansClusterer.cs | 2 +- Assets/Tests/EditMode/AgglomerativeClustererTest.cs | 4 ++-- Assets/Tests/EditMode/ClusterTest.cs | 4 ++-- Assets/Tests/EditMode/KMeansClustererTest.cs | 4 ++-- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Assets/Scripts/Algorithms/AgglomerativeClusterer.cs b/Assets/Scripts/Algorithms/AgglomerativeClusterer.cs index 0a9c4578..c33cd311 100644 --- a/Assets/Scripts/Algorithms/AgglomerativeClusterer.cs +++ b/Assets/Scripts/Algorithms/AgglomerativeClusterer.cs @@ -1,7 +1,6 @@ using System.Collections; using System.Collections.Generic; using UnityEngine; -using System; // The agglomerative clusterer class performs agglomerative clustering with the stopping condition // given by the size and radius constraints. diff --git a/Assets/Scripts/Algorithms/KMeansClusterer.cs b/Assets/Scripts/Algorithms/KMeansClusterer.cs index b38eb991..f870dd3e 100644 --- a/Assets/Scripts/Algorithms/KMeansClusterer.cs +++ b/Assets/Scripts/Algorithms/KMeansClusterer.cs @@ -1,7 +1,7 @@ +using System; using System.Collections; using System.Collections.Generic; using UnityEngine; -using System; // The k-means clusterer class performs k-means clustering. public class KMeansClusterer : IClusterer { diff --git a/Assets/Tests/EditMode/AgglomerativeClustererTest.cs b/Assets/Tests/EditMode/AgglomerativeClustererTest.cs index e294da77..472e0a48 100644 --- a/Assets/Tests/EditMode/AgglomerativeClustererTest.cs +++ b/Assets/Tests/EditMode/AgglomerativeClustererTest.cs @@ -1,9 +1,9 @@ using NUnit.Framework; -using UnityEngine; -using UnityEngine.TestTools; using System.Collections; using System.Collections.Generic; using System.Linq; +using UnityEngine; +using UnityEngine.TestTools; public class AgglomerativeClustererTest { public static readonly List Points = new List { diff --git a/Assets/Tests/EditMode/ClusterTest.cs b/Assets/Tests/EditMode/ClusterTest.cs index 03fadc48..192cc8ab 100644 --- a/Assets/Tests/EditMode/ClusterTest.cs +++ b/Assets/Tests/EditMode/ClusterTest.cs @@ -1,8 +1,8 @@ using NUnit.Framework; -using UnityEngine; -using UnityEngine.TestTools; using System.Collections; using System.Collections.Generic; +using UnityEngine; +using UnityEngine.TestTools; public class ClusterTest { public static Cluster GenerateCluster(in IReadOnlyList points) { diff --git a/Assets/Tests/EditMode/KMeansClustererTest.cs b/Assets/Tests/EditMode/KMeansClustererTest.cs index 259fd796..6340e27c 100644 --- a/Assets/Tests/EditMode/KMeansClustererTest.cs +++ b/Assets/Tests/EditMode/KMeansClustererTest.cs @@ -1,9 +1,9 @@ using NUnit.Framework; -using UnityEngine; -using UnityEngine.TestTools; using System.Collections; using System.Collections.Generic; using System.Linq; +using UnityEngine; +using UnityEngine.TestTools; public class KMeansClustererTest { public static readonly List Points = new List { From 1a8a05cc171b88c4470302e61897df47ca682248 Mon Sep 17 00:00:00 2001 From: Titan Yuan Date: Sat, 4 Jan 2025 22:36:33 -0800 Subject: [PATCH 06/13] Move clustering algorithms into separate directory --- Assets/Scripts/Algorithms/AgglomerativeClusterer.cs.meta | 2 -- Assets/Scripts/Algorithms/Cluster.cs.meta | 2 -- Assets/Scripts/Algorithms/Clusterer.cs.meta | 2 -- Assets/Scripts/Algorithms/Clustering.meta | 8 ++++++++ .../Algorithms/{ => Clustering}/AgglomerativeClusterer.cs | 0 .../Algorithms/Clustering/AgglomerativeClusterer.cs.meta | 2 ++ Assets/Scripts/Algorithms/{ => Clustering}/Cluster.cs | 0 Assets/Scripts/Algorithms/Clustering/Cluster.cs.meta | 2 ++ Assets/Scripts/Algorithms/{ => Clustering}/Clusterer.cs | 0 Assets/Scripts/Algorithms/Clustering/Clusterer.cs.meta | 2 ++ .../Algorithms/{ => Clustering}/KMeansClusterer.cs | 0 .../Scripts/Algorithms/Clustering/KMeansClusterer.cs.meta | 2 ++ Assets/Scripts/Algorithms/KMeansClusterer.cs.meta | 2 -- Assets/Tests/EditMode/KMeansClustererTest.cs | 4 ---- 14 files changed, 16 insertions(+), 12 deletions(-) delete mode 100644 Assets/Scripts/Algorithms/AgglomerativeClusterer.cs.meta delete mode 100644 Assets/Scripts/Algorithms/Cluster.cs.meta delete mode 100644 Assets/Scripts/Algorithms/Clusterer.cs.meta create mode 100644 Assets/Scripts/Algorithms/Clustering.meta rename Assets/Scripts/Algorithms/{ => Clustering}/AgglomerativeClusterer.cs (100%) create mode 100644 Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs.meta rename Assets/Scripts/Algorithms/{ => Clustering}/Cluster.cs (100%) create mode 100644 Assets/Scripts/Algorithms/Clustering/Cluster.cs.meta rename Assets/Scripts/Algorithms/{ => Clustering}/Clusterer.cs (100%) create mode 100644 Assets/Scripts/Algorithms/Clustering/Clusterer.cs.meta rename Assets/Scripts/Algorithms/{ => Clustering}/KMeansClusterer.cs (100%) create mode 100644 Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs.meta delete mode 100644 Assets/Scripts/Algorithms/KMeansClusterer.cs.meta diff --git a/Assets/Scripts/Algorithms/AgglomerativeClusterer.cs.meta b/Assets/Scripts/Algorithms/AgglomerativeClusterer.cs.meta deleted file mode 100644 index e9660687..00000000 --- a/Assets/Scripts/Algorithms/AgglomerativeClusterer.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 847699c106b964d89b4fa0cbc78e6694 \ No newline at end of file diff --git a/Assets/Scripts/Algorithms/Cluster.cs.meta b/Assets/Scripts/Algorithms/Cluster.cs.meta deleted file mode 100644 index 145d5082..00000000 --- a/Assets/Scripts/Algorithms/Cluster.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 6466e5b76f1704b55ae0ea8493b999a0 \ No newline at end of file diff --git a/Assets/Scripts/Algorithms/Clusterer.cs.meta b/Assets/Scripts/Algorithms/Clusterer.cs.meta deleted file mode 100644 index 19e3d02b..00000000 --- a/Assets/Scripts/Algorithms/Clusterer.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 91be483c8155f45688387ac68e79046f \ No newline at end of file diff --git a/Assets/Scripts/Algorithms/Clustering.meta b/Assets/Scripts/Algorithms/Clustering.meta new file mode 100644 index 00000000..821ce142 --- /dev/null +++ b/Assets/Scripts/Algorithms/Clustering.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8e4bdae9b86b243de9987ac58f316992 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Algorithms/AgglomerativeClusterer.cs b/Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs similarity index 100% rename from Assets/Scripts/Algorithms/AgglomerativeClusterer.cs rename to Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs diff --git a/Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs.meta b/Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs.meta new file mode 100644 index 00000000..47613e98 --- /dev/null +++ b/Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e0c0460dd55b54f39bf7ca7878411679 \ No newline at end of file diff --git a/Assets/Scripts/Algorithms/Cluster.cs b/Assets/Scripts/Algorithms/Clustering/Cluster.cs similarity index 100% rename from Assets/Scripts/Algorithms/Cluster.cs rename to Assets/Scripts/Algorithms/Clustering/Cluster.cs diff --git a/Assets/Scripts/Algorithms/Clustering/Cluster.cs.meta b/Assets/Scripts/Algorithms/Clustering/Cluster.cs.meta new file mode 100644 index 00000000..641b1e4a --- /dev/null +++ b/Assets/Scripts/Algorithms/Clustering/Cluster.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 99d3dec10e6c64a86a7cd57009cf1c31 \ No newline at end of file diff --git a/Assets/Scripts/Algorithms/Clusterer.cs b/Assets/Scripts/Algorithms/Clustering/Clusterer.cs similarity index 100% rename from Assets/Scripts/Algorithms/Clusterer.cs rename to Assets/Scripts/Algorithms/Clustering/Clusterer.cs diff --git a/Assets/Scripts/Algorithms/Clustering/Clusterer.cs.meta b/Assets/Scripts/Algorithms/Clustering/Clusterer.cs.meta new file mode 100644 index 00000000..b80bb31d --- /dev/null +++ b/Assets/Scripts/Algorithms/Clustering/Clusterer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b622dd33f3bbe4622bd82fbc79f64cf7 \ No newline at end of file diff --git a/Assets/Scripts/Algorithms/KMeansClusterer.cs b/Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs similarity index 100% rename from Assets/Scripts/Algorithms/KMeansClusterer.cs rename to Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs diff --git a/Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs.meta b/Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs.meta new file mode 100644 index 00000000..4823728e --- /dev/null +++ b/Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 239318f01460f416792b3fd0e5165fee \ No newline at end of file diff --git a/Assets/Scripts/Algorithms/KMeansClusterer.cs.meta b/Assets/Scripts/Algorithms/KMeansClusterer.cs.meta deleted file mode 100644 index e0f9abb8..00000000 --- a/Assets/Scripts/Algorithms/KMeansClusterer.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: ebb68733cf6dd4516848a1b7f5ff11e3 \ No newline at end of file diff --git a/Assets/Tests/EditMode/KMeansClustererTest.cs b/Assets/Tests/EditMode/KMeansClustererTest.cs index 6340e27c..39c03a97 100644 --- a/Assets/Tests/EditMode/KMeansClustererTest.cs +++ b/Assets/Tests/EditMode/KMeansClustererTest.cs @@ -72,9 +72,5 @@ public void TestSmallRadius() { clusterer.Cluster(); Assert.AreEqual(clusterer.Clusters.Count, 2); List clusters = clusterer.Clusters.OrderBy(cluster => cluster.Position[1]).ToList(); - Assert.AreEqual(clusters[0].Size(), 2); - Assert.AreEqual(clusters[0].Position, new Vector3(0, 0.5f, 0)); - Assert.AreEqual(clusters[1].Size(), 2); - Assert.AreEqual(clusters[1].Position, new Vector3(0, 2, 0)); } } From 3392f3ed0a0ebfbfb938d4a53ae18ce7f188e0f2 Mon Sep 17 00:00:00 2001 From: Titan Yuan Date: Sun, 5 Jan 2025 23:03:01 -0800 Subject: [PATCH 07/13] Fix capitalization conventions for private and protected variabes --- .../Clustering/AgglomerativeClusterer.cs | 34 ++++++------- .../Scripts/Algorithms/Clustering/Cluster.cs | 32 ++++++------ .../Algorithms/Clustering/Clusterer.cs | 18 +++---- .../Algorithms/Clustering/KMeansClusterer.cs | 51 ++++++++++--------- .../EditMode/AgglomerativeClustererTest.cs | 8 +-- Assets/Tests/EditMode/ClusterTest.cs | 6 +-- Assets/Tests/EditMode/KMeansClustererTest.cs | 4 +- 7 files changed, 77 insertions(+), 76 deletions(-) diff --git a/Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs b/Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs index c33cd311..72451ab2 100644 --- a/Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs +++ b/Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs @@ -11,24 +11,24 @@ public AgglomerativeClusterer(List points, int maxSize, float maxRadius // Cluster the points. public override void Cluster() { // Add a cluster for every point. - foreach (var point in points) { + foreach (var point in _points) { Cluster cluster = new Cluster(point); cluster.AddPoint(point); - clusters.Add(cluster); + _clusters.Add(cluster); } // Create a set containing all valid cluster indices. HashSet validClusterIndices = new HashSet(); - for (int i = 0; i < clusters.Count; ++i) { + for (int i = 0; i < _clusters.Count; ++i) { validClusterIndices.Add(i); } // Find the pairwise distances between all clusters. // The upper triangular half of the distances matrix is unused. - float[,] distances = new float[clusters.Count, clusters.Count]; - for (int i = 0; i < clusters.Count; ++i) { + float[,] distances = new float[_clusters.Count, _clusters.Count]; + for (int i = 0; i < _clusters.Count; ++i) { for (int j = 0; j < i; ++j) { - distances[i, j] = Vector3.Distance(clusters[i].Position, clusters[j].Position); + distances[i, j] = Vector3.Distance(_clusters[i].Coordinates, _clusters[j].Coordinates); } } @@ -37,7 +37,7 @@ public override void Cluster() { float minDistance = Mathf.Infinity; int clusterIndex1 = -1; int clusterIndex2 = -1; - for (int i = 0; i < clusters.Count; ++i) { + for (int i = 0; i < _clusters.Count; ++i) { for (int j = 0; j < i; ++j) { if (distances[i, j] < minDistance) { minDistance = distances[i, j]; @@ -50,12 +50,12 @@ public override void Cluster() { // Check whether the minimum distance exceeds the maximum cluster radius, in which case the // algorithm has converged. This produces a conservative solution because the radius of a // merged cluster is less than or equal to the sum of the original cluster radii. - if (minDistance >= maxRadius) { + if (minDistance >= _maxRadius) { break; } // Check whether merging the two clusters would violate the size constraint. - if (clusters[clusterIndex1].Size() + clusters[clusterIndex2].Size() > maxSize) { + if (_clusters[clusterIndex1].Size() + _clusters[clusterIndex2].Size() > _maxSize) { distances[clusterIndex1, clusterIndex2] = Mathf.Infinity; continue; } @@ -63,8 +63,8 @@ public override void Cluster() { // Merge the two clusters together. int minClusterIndex = Mathf.Min(clusterIndex1, clusterIndex2); int maxClusterIndex = Mathf.Max(clusterIndex1, clusterIndex2); - clusters[minClusterIndex].Merge(clusters[maxClusterIndex]); - clusters[minClusterIndex].Recenter(); + _clusters[minClusterIndex].Merge(_clusters[maxClusterIndex]); + _clusters[minClusterIndex].Recenter(); validClusterIndices.Remove(maxClusterIndex); // Update the distances matrix using the distance between the cluster centroids. @@ -72,27 +72,27 @@ public override void Cluster() { for (int i = 0; i < minClusterIndex; ++i) { if (distances[minClusterIndex, i] < Mathf.Infinity) { distances[minClusterIndex, i] = - Vector3.Distance(clusters[minClusterIndex].Position, clusters[i].Position); + Vector3.Distance(_clusters[minClusterIndex].Coordinates, _clusters[i].Coordinates); } } - for (int i = minClusterIndex + 1; i < clusters.Count; ++i) { + for (int i = minClusterIndex + 1; i < _clusters.Count; ++i) { if (distances[i, minClusterIndex] < Mathf.Infinity) { distances[i, minClusterIndex] = - Vector3.Distance(clusters[minClusterIndex].Position, clusters[i].Position); + Vector3.Distance(_clusters[minClusterIndex].Coordinates, _clusters[i].Coordinates); } } for (int i = 0; i < maxClusterIndex; ++i) { distances[maxClusterIndex, i] = Mathf.Infinity; } - for (int i = maxClusterIndex + 1; i < clusters.Count; ++i) { + for (int i = maxClusterIndex + 1; i < _clusters.Count; ++i) { distances[i, maxClusterIndex] = Mathf.Infinity; } } // Select only the valid clusters. - for (int i = clusters.Count - 1; i >= 0; --i) { + for (int i = _clusters.Count - 1; i >= 0; --i) { if (!validClusterIndices.Contains(i)) { - clusters.RemoveAt(i); + _clusters.RemoveAt(i); } } } diff --git a/Assets/Scripts/Algorithms/Clustering/Cluster.cs b/Assets/Scripts/Algorithms/Clustering/Cluster.cs index f7a5b39c..68f85fcc 100644 --- a/Assets/Scripts/Algorithms/Clustering/Cluster.cs +++ b/Assets/Scripts/Algorithms/Clustering/Cluster.cs @@ -5,30 +5,30 @@ // The cluster class represents a collection of points with a defined centroid. public class Cluster { - // Position of the cluster. - private Vector3 position = Vector3.zero; + // Coordinates of the cluster. + private Vector3 _coordinates = Vector3.zero; // List of points in the cluster. - private List points = new List(); + private List _points = new List(); public Cluster() {} - public Cluster(in Vector3 position) { - this.position = position; + public Cluster(in Vector3 coordinates) { + _coordinates = coordinates; } - // Get the cluster position. - public Vector3 Position { - get { return position; } + // Get the cluster coordinates. + public Vector3 Coordinates { + get { return _coordinates; } } // Get the list of points. public IReadOnlyList Points { - get { return points; } + get { return _points; } } // Return the size of the cluster. public int Size() { - return points.Count; + return _points.Count; } // Check whether the cluster is empty. @@ -43,7 +43,7 @@ public float Radius() { } Vector3 centroid = Centroid(); - return points.Max(point => Vector3.Distance(centroid, point)); + return _points.Max(point => Vector3.Distance(centroid, point)); } // Calculate the centroid of the cluster. @@ -53,28 +53,28 @@ public Vector3 Centroid() { } Vector3 centroid = Vector3.zero; - foreach (var point in points) { + foreach (var point in _points) { centroid += point; } - centroid /= points.Count; + centroid /= _points.Count; return centroid; } // Recenter the cluster's centroid to be the mean of all points in the cluster. public void Recenter() { - position = Centroid(); + _coordinates = Centroid(); } // Add a point to the cluster. // This function does not update the centroid of the cluster. public void AddPoint(in Vector3 point) { - points.Add(point); + _points.Add(point); } // Add multiple points to the cluster. // This function does not update the centroid of the cluster. public void AddPoints(in IReadOnlyList otherPoints) { - points.AddRange(otherPoints); + _points.AddRange(otherPoints); } // Merge another cluster into this one. diff --git a/Assets/Scripts/Algorithms/Clustering/Clusterer.cs b/Assets/Scripts/Algorithms/Clustering/Clusterer.cs index eb5295dd..3337ad50 100644 --- a/Assets/Scripts/Algorithms/Clustering/Clusterer.cs +++ b/Assets/Scripts/Algorithms/Clustering/Clusterer.cs @@ -5,23 +5,23 @@ // The clusterer class is an interface for clustering algorithms. public abstract class IClusterer { // List of points to cluster. - protected List points = new List(); + protected List _points = new List(); // List of clusters. - protected List clusters = new List(); + protected List _clusters = new List(); public IClusterer(List points) { - this.points = points; + _points = points; } // Get the list of points. public IReadOnlyList Points { - get { return points; } + get { return _points; } } // Get the list of clusters. public IReadOnlyList Clusters { - get { return clusters; } + get { return _clusters; } } // Cluster the points. @@ -34,14 +34,14 @@ public IReadOnlyList Clusters { // assigned points. public abstract class ISizeAndRadiusConstrainedClusterer : IClusterer { // Maximum cluster size. - protected readonly int maxSize = 0; + protected readonly int _maxSize = 0; // Maximum cluster radius. - protected readonly float maxRadius = 0; + protected readonly float _maxRadius = 0; public ISizeAndRadiusConstrainedClusterer(List points, int maxSize, float maxRadius) : base(points) { - this.maxSize = maxSize; - this.maxRadius = maxRadius; + _maxSize = maxSize; + _maxRadius = maxRadius; } } diff --git a/Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs b/Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs index f870dd3e..def606ec 100644 --- a/Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs +++ b/Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs @@ -8,14 +8,14 @@ public class KMeansClusterer : IClusterer { public const float Epsilon = 1e-3f; // Number of clusters. - private int k = 0; + private int _k = 0; // Maximum number of iterations. - private int maxIterations = 20; + private int _maxIterations = 20; public KMeansClusterer(List points, int k, int maxIterations = 20) : base(points) { - this.k = k; - this.maxIterations = maxIterations; + _k = k; + _maxIterations = maxIterations; } // Cluster the points. @@ -23,36 +23,37 @@ public override void Cluster() { // Initialize the clusters with centroids at random points. // Perform Fisher-Yates shuffling to find k random points. System.Random random = new System.Random(); - for (int i = points.Count - 1; i >= points.Count - k; --i) { + for (int i = _points.Count - 1; i >= _points.Count - _k; --i) { int j = random.Next(i + 1); - (points[i], points[j]) = (points[j], points[i]); + (_points[i], _points[j]) = (_points[j], _points[i]); } - for (int i = points.Count - 1; i >= points.Count - k; --i) { - clusters.Add(new Cluster(points[i])); + for (int i = _points.Count - 1; i >= _points.Count - _k; --i) { + _clusters.Add(new Cluster(_points[i])); } bool converged = false; int iteration = 0; - while (!converged && iteration < maxIterations) { + while (!converged && iteration < _maxIterations) { AssignPointsToCluster(); // Calculate the new clusters as the mean of all assigned points. converged = true; - for (int clusterIndex = 0; clusterIndex < clusters.Count; ++clusterIndex) { + for (int clusterIndex = 0; clusterIndex < _clusters.Count; ++clusterIndex) { Cluster newCluster; - if (clusters[clusterIndex].IsEmpty()) { - int pointIndex = random.Next(points.Count); - newCluster = new Cluster(points[pointIndex]); + if (_clusters[clusterIndex].IsEmpty()) { + int pointIndex = random.Next(_points.Count); + newCluster = new Cluster(_points[pointIndex]); } else { - newCluster = new Cluster(clusters[clusterIndex].Centroid()); + newCluster = new Cluster(_clusters[clusterIndex].Centroid()); } // Check whether the algorithm has converged by checking whether the cluster has moved. - if (Vector3.Distance(newCluster.Position, clusters[clusterIndex].Position) > Epsilon) { + if (Vector3.Distance(newCluster.Coordinates, _clusters[clusterIndex].Coordinates) > + Epsilon) { converged = false; } - clusters[clusterIndex] = newCluster; + _clusters[clusterIndex] = newCluster; } ++iteration; @@ -63,17 +64,17 @@ public override void Cluster() { private void AssignPointsToCluster() { // Determine the closest centroid to each point. - foreach (var point in points) { + foreach (var point in _points) { float minDistance = Mathf.Infinity; int minIndex = -1; - for (int clusterIndex = 0; clusterIndex < clusters.Count; ++clusterIndex) { - float distance = Vector3.Distance(clusters[clusterIndex].Position, point); + for (int clusterIndex = 0; clusterIndex < _clusters.Count; ++clusterIndex) { + float distance = Vector3.Distance(_clusters[clusterIndex].Coordinates, point); if (distance < minDistance) { minDistance = distance; minIndex = clusterIndex; } } - clusters[minIndex].AddPoint(point); + _clusters[minIndex].AddPoint(point); } } } @@ -86,20 +87,20 @@ public ConstrainedKMeansClusterer(List points, int maxSize, float maxRa // Cluster the points. public override void Cluster() { - int numClusters = (int)Mathf.Ceil(points.Count / maxSize); + int numClusters = (int)Mathf.Ceil(_points.Count / _maxSize); KMeansClusterer clusterer; while (true) { - clusterer = new KMeansClusterer(points, numClusters); + clusterer = new KMeansClusterer(_points, numClusters); clusterer.Cluster(); // Count the number of over-populated and over-sized clusters. int numOverPopulatedClusters = 0; int numOverSizedClusters = 0; foreach (var cluster in clusterer.Clusters) { - if (cluster.Size() > maxSize) { + if (cluster.Size() > _maxSize) { ++numOverPopulatedClusters; } - if (cluster.Radius() > maxRadius) { + if (cluster.Radius() > _maxRadius) { ++numOverSizedClusters; } } @@ -112,6 +113,6 @@ public override void Cluster() { numClusters += (int)Mathf.Ceil(Mathf.Max(numOverPopulatedClusters, numOverSizedClusters) / 2.0f); } - clusters = new List(clusterer.Clusters); + _clusters = new List(clusterer.Clusters); } } diff --git a/Assets/Tests/EditMode/AgglomerativeClustererTest.cs b/Assets/Tests/EditMode/AgglomerativeClustererTest.cs index 472e0a48..99219c27 100644 --- a/Assets/Tests/EditMode/AgglomerativeClustererTest.cs +++ b/Assets/Tests/EditMode/AgglomerativeClustererTest.cs @@ -52,12 +52,12 @@ public void TestSmallRadius() { new AgglomerativeClusterer(Points, maxSize: Points.Count, maxRadius: 1); clusterer.Cluster(); Assert.AreEqual(clusterer.Clusters.Count, 3); - List clusters = clusterer.Clusters.OrderBy(cluster => cluster.Position[1]).ToList(); + List clusters = clusterer.Clusters.OrderBy(cluster => cluster.Coordinates[1]).ToList(); Assert.AreEqual(clusters[0].Size(), 1); - Assert.AreEqual(clusters[0].Position, new Vector3(0, 0, 0)); + Assert.AreEqual(clusters[0].Coordinates, new Vector3(0, 0, 0)); Assert.AreEqual(clusters[1].Size(), 2); - Assert.AreEqual(clusters[1].Position, new Vector3(0, 1.25f, 0)); + Assert.AreEqual(clusters[1].Coordinates, new Vector3(0, 1.25f, 0)); Assert.AreEqual(clusters[2].Size(), 1); - Assert.AreEqual(clusters[2].Position, new Vector3(0, 2.5f, 0)); + Assert.AreEqual(clusters[2].Coordinates, new Vector3(0, 2.5f, 0)); } } diff --git a/Assets/Tests/EditMode/ClusterTest.cs b/Assets/Tests/EditMode/ClusterTest.cs index 192cc8ab..4c89d8d5 100644 --- a/Assets/Tests/EditMode/ClusterTest.cs +++ b/Assets/Tests/EditMode/ClusterTest.cs @@ -68,9 +68,9 @@ public void TestRecenter() { } Cluster cluster = GenerateCluster(points); cluster.AddPoint(new Vector3(10, -10, 0)); - Assert.AreNotEqual(cluster.Position, new Vector3(1, -1, 0)); + Assert.AreNotEqual(cluster.Coordinates, new Vector3(1, -1, 0)); cluster.Recenter(); - Assert.AreEqual(cluster.Position, new Vector3(1, -1, 0)); + Assert.AreEqual(cluster.Coordinates, new Vector3(1, -1, 0)); } [Test] @@ -91,6 +91,6 @@ public void TestMerge() { cluster1.Merge(cluster2); cluster1.Recenter(); Assert.AreEqual(cluster1.Size(), size1 + size2); - Assert.AreEqual(cluster1.Position, (centroid1 + centroid2) / 2); + Assert.AreEqual(cluster1.Coordinates, (centroid1 + centroid2) / 2); } } diff --git a/Assets/Tests/EditMode/KMeansClustererTest.cs b/Assets/Tests/EditMode/KMeansClustererTest.cs index 39c03a97..e87b90aa 100644 --- a/Assets/Tests/EditMode/KMeansClustererTest.cs +++ b/Assets/Tests/EditMode/KMeansClustererTest.cs @@ -19,7 +19,7 @@ public void TestSingleCluster() { clusterer.Cluster(); Cluster cluster = clusterer.Clusters[0]; Assert.AreEqual(cluster.Size(), Points.Count); - Assert.AreEqual(cluster.Position, new Vector3(0, 1.25f, 0)); + Assert.AreEqual(cluster.Coordinates, new Vector3(0, 1.25f, 0)); Assert.AreEqual(cluster.Centroid(), new Vector3(0, 1.25f, 0)); } } @@ -71,6 +71,6 @@ public void TestSmallRadius() { new ConstrainedKMeansClusterer(Points, maxSize: Points.Count, maxRadius: 1); clusterer.Cluster(); Assert.AreEqual(clusterer.Clusters.Count, 2); - List clusters = clusterer.Clusters.OrderBy(cluster => cluster.Position[1]).ToList(); + List clusters = clusterer.Clusters.OrderBy(cluster => cluster.Coordinates[1]).ToList(); } } From 37513598a1abef86f1b1f31ca6696a060395e384 Mon Sep 17 00:00:00 2001 From: Titan Yuan Date: Mon, 6 Jan 2025 15:04:08 -0800 Subject: [PATCH 08/13] Change from Vector3 points to GameObjects --- .../Clustering/AgglomerativeClusterer.cs | 14 ++--- .../Scripts/Algorithms/Clustering/Cluster.cs | 41 +++++++------- .../Algorithms/Clustering/Clusterer.cs | 24 ++++----- .../Algorithms/Clustering/KMeansClusterer.cs | 47 ++++++++-------- .../EditMode/AgglomerativeClustererTest.cs | 30 ++++++----- Assets/Tests/EditMode/ClusterTest.cs | 53 ++++++++++--------- Assets/Tests/EditMode/KMeansClustererTest.cs | 50 ++++++++++------- 7 files changed, 143 insertions(+), 116 deletions(-) diff --git a/Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs b/Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs index 72451ab2..52967aa0 100644 --- a/Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs +++ b/Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs @@ -5,15 +5,15 @@ // The agglomerative clusterer class performs agglomerative clustering with the stopping condition // given by the size and radius constraints. public class AgglomerativeClusterer : ISizeAndRadiusConstrainedClusterer { - public AgglomerativeClusterer(List points, int maxSize, float maxRadius) - : base(points, maxSize, maxRadius) {} + public AgglomerativeClusterer(List objects, int maxSize, float maxRadius) + : base(objects, maxSize, maxRadius) {} - // Cluster the points. + // Cluster the game objects. public override void Cluster() { - // Add a cluster for every point. - foreach (var point in _points) { - Cluster cluster = new Cluster(point); - cluster.AddPoint(point); + // Add a cluster for every game object. + foreach (var obj in _objects) { + Cluster cluster = new Cluster(obj); + cluster.AddObject(obj); _clusters.Add(cluster); } diff --git a/Assets/Scripts/Algorithms/Clustering/Cluster.cs b/Assets/Scripts/Algorithms/Clustering/Cluster.cs index 68f85fcc..547d9296 100644 --- a/Assets/Scripts/Algorithms/Clustering/Cluster.cs +++ b/Assets/Scripts/Algorithms/Clustering/Cluster.cs @@ -3,32 +3,35 @@ using System.Linq; using UnityEngine; -// The cluster class represents a collection of points with a defined centroid. +// The cluster class represents a collection of game objects. public class Cluster { // Coordinates of the cluster. private Vector3 _coordinates = Vector3.zero; - // List of points in the cluster. - private List _points = new List(); + // List of game objects in the cluster. + private List _objects = new List(); public Cluster() {} public Cluster(in Vector3 coordinates) { _coordinates = coordinates; } + public Cluster(in GameObject obj) { + _coordinates = obj.transform.position; + } // Get the cluster coordinates. public Vector3 Coordinates { get { return _coordinates; } } - // Get the list of points. - public IReadOnlyList Points { - get { return _points; } + // Get the list of game objects. + public IReadOnlyList Objects { + get { return _objects; } } // Return the size of the cluster. public int Size() { - return _points.Count; + return _objects.Count; } // Check whether the cluster is empty. @@ -43,7 +46,7 @@ public float Radius() { } Vector3 centroid = Centroid(); - return _points.Max(point => Vector3.Distance(centroid, point)); + return _objects.Max(obj => Vector3.Distance(centroid, obj.transform.position)); } // Calculate the centroid of the cluster. @@ -53,33 +56,33 @@ public Vector3 Centroid() { } Vector3 centroid = Vector3.zero; - foreach (var point in _points) { - centroid += point; + foreach (var obj in _objects) { + centroid += obj.transform.position; } - centroid /= _points.Count; + centroid /= _objects.Count; return centroid; } - // Recenter the cluster's centroid to be the mean of all points in the cluster. + // Recenter the cluster's centroid to be the mean of all game objects' positions in the cluster. public void Recenter() { _coordinates = Centroid(); } - // Add a point to the cluster. + // Add a game object to the cluster. // This function does not update the centroid of the cluster. - public void AddPoint(in Vector3 point) { - _points.Add(point); + public void AddObject(in GameObject obj) { + _objects.Add(obj); } - // Add multiple points to the cluster. + // Add multiple game objects to the cluster. // This function does not update the centroid of the cluster. - public void AddPoints(in IReadOnlyList otherPoints) { - _points.AddRange(otherPoints); + public void AddObjects(in IReadOnlyList objects) { + _objects.AddRange(objects); } // Merge another cluster into this one. // This function does not update the centroid of the cluster. public void Merge(in Cluster cluster) { - AddPoints(cluster.Points); + AddObjects(cluster.Objects); } } diff --git a/Assets/Scripts/Algorithms/Clustering/Clusterer.cs b/Assets/Scripts/Algorithms/Clustering/Clusterer.cs index 3337ad50..5be4aa28 100644 --- a/Assets/Scripts/Algorithms/Clustering/Clusterer.cs +++ b/Assets/Scripts/Algorithms/Clustering/Clusterer.cs @@ -4,19 +4,19 @@ // The clusterer class is an interface for clustering algorithms. public abstract class IClusterer { - // List of points to cluster. - protected List _points = new List(); + // List of game objects to cluster. + protected List _objects = new List(); // List of clusters. protected List _clusters = new List(); - public IClusterer(List points) { - _points = points; + public IClusterer(List objects) { + _objects = objects; } - // Get the list of points. - public IReadOnlyList Points { - get { return _points; } + // Get the list of game objects. + public IReadOnlyList Objects { + get { return _objects; } } // Get the list of clusters. @@ -24,14 +24,14 @@ public IReadOnlyList Clusters { get { return _clusters; } } - // Cluster the points. + // Cluster the game objects. public abstract void Cluster(); } // The size and radius-constrained clusterer class is an interface for clustering algorithms with -// size and radius constraints. The size is defined as the maximum number of points within a +// size and radius constraints. The size is defined as the maximum number of game objects within a // cluster, and the radius denotes the maximum distance from the cluster's centroid to any of its -// assigned points. +// assigned game objects. public abstract class ISizeAndRadiusConstrainedClusterer : IClusterer { // Maximum cluster size. protected readonly int _maxSize = 0; @@ -39,8 +39,8 @@ public abstract class ISizeAndRadiusConstrainedClusterer : IClusterer { // Maximum cluster radius. protected readonly float _maxRadius = 0; - public ISizeAndRadiusConstrainedClusterer(List points, int maxSize, float maxRadius) - : base(points) { + public ISizeAndRadiusConstrainedClusterer(List objects, int maxSize, float maxRadius) + : base(objects) { _maxSize = maxSize; _maxRadius = maxRadius; } diff --git a/Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs b/Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs index def606ec..e086e526 100644 --- a/Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs +++ b/Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs @@ -13,36 +13,36 @@ public class KMeansClusterer : IClusterer { // Maximum number of iterations. private int _maxIterations = 20; - public KMeansClusterer(List points, int k, int maxIterations = 20) : base(points) { + public KMeansClusterer(List objects, int k, int maxIterations = 20) : base(objects) { _k = k; _maxIterations = maxIterations; } - // Cluster the points. + // Cluster the game objects. public override void Cluster() { - // Initialize the clusters with centroids at random points. - // Perform Fisher-Yates shuffling to find k random points. + // Initialize the clusters with centroids located at random game objects. + // Perform Fisher-Yates shuffling to find k random game objects. System.Random random = new System.Random(); - for (int i = _points.Count - 1; i >= _points.Count - _k; --i) { + for (int i = _objects.Count - 1; i >= _objects.Count - _k; --i) { int j = random.Next(i + 1); - (_points[i], _points[j]) = (_points[j], _points[i]); + (_objects[i], _objects[j]) = (_objects[j], _objects[i]); } - for (int i = _points.Count - 1; i >= _points.Count - _k; --i) { - _clusters.Add(new Cluster(_points[i])); + for (int i = _objects.Count - 1; i >= _objects.Count - _k; --i) { + _clusters.Add(new Cluster(_objects[i])); } bool converged = false; int iteration = 0; while (!converged && iteration < _maxIterations) { - AssignPointsToCluster(); + AssignObjectsToCluster(); - // Calculate the new clusters as the mean of all assigned points. + // Calculate the new clusters as the mean of all assigned game objects. converged = true; for (int clusterIndex = 0; clusterIndex < _clusters.Count; ++clusterIndex) { Cluster newCluster; if (_clusters[clusterIndex].IsEmpty()) { - int pointIndex = random.Next(_points.Count); - newCluster = new Cluster(_points[pointIndex]); + int objectIndex = random.Next(_objects.Count); + newCluster = new Cluster(_objects[objectIndex]); } else { newCluster = new Cluster(_clusters[clusterIndex].Centroid()); } @@ -59,22 +59,23 @@ public override void Cluster() { ++iteration; } - AssignPointsToCluster(); + AssignObjectsToCluster(); } - private void AssignPointsToCluster() { - // Determine the closest centroid to each point. - foreach (var point in _points) { + private void AssignObjectsToCluster() { + // Determine the closest centroid to each game object. + foreach (var obj in _objects) { float minDistance = Mathf.Infinity; int minIndex = -1; for (int clusterIndex = 0; clusterIndex < _clusters.Count; ++clusterIndex) { - float distance = Vector3.Distance(_clusters[clusterIndex].Coordinates, point); + float distance = + Vector3.Distance(_clusters[clusterIndex].Coordinates, obj.transform.position); if (distance < minDistance) { minDistance = distance; minIndex = clusterIndex; } } - _clusters[minIndex].AddPoint(point); + _clusters[minIndex].AddObject(obj); } } } @@ -82,15 +83,15 @@ private void AssignPointsToCluster() { // The constrained k-means clusterer class performs k-means clustering under size and radius // constraints. public class ConstrainedKMeansClusterer : ISizeAndRadiusConstrainedClusterer { - public ConstrainedKMeansClusterer(List points, int maxSize, float maxRadius) - : base(points, maxSize, maxRadius) {} + public ConstrainedKMeansClusterer(List objects, int maxSize, float maxRadius) + : base(objects, maxSize, maxRadius) {} - // Cluster the points. + // Cluster the game objects. public override void Cluster() { - int numClusters = (int)Mathf.Ceil(_points.Count / _maxSize); + int numClusters = (int)Mathf.Ceil(_objects.Count / _maxSize); KMeansClusterer clusterer; while (true) { - clusterer = new KMeansClusterer(_points, numClusters); + clusterer = new KMeansClusterer(_objects, numClusters); clusterer.Cluster(); // Count the number of over-populated and over-sized clusters. diff --git a/Assets/Tests/EditMode/AgglomerativeClustererTest.cs b/Assets/Tests/EditMode/AgglomerativeClustererTest.cs index 99219c27..8ba24b24 100644 --- a/Assets/Tests/EditMode/AgglomerativeClustererTest.cs +++ b/Assets/Tests/EditMode/AgglomerativeClustererTest.cs @@ -6,30 +6,36 @@ using UnityEngine.TestTools; public class AgglomerativeClustererTest { - public static readonly List Points = new List { - new Vector3(0, 0, 0), - new Vector3(0, 1, 0), - new Vector3(0, 1.5f, 0), - new Vector3(0, 2.5f, 0), + public static GameObject GenerateObject(in Vector3 position) { + GameObject obj = new GameObject(); + obj.transform.position = position; + return obj; + } + + public static readonly List Objects = new List { + GenerateObject(new Vector3(0, 0, 0)), + GenerateObject(new Vector3(0, 1, 0)), + GenerateObject(new Vector3(0, 1.5f, 0)), + GenerateObject(new Vector3(0, 2.5f, 0)), }; [Test] public void TestSingleCluster() { AgglomerativeClusterer clusterer = - new AgglomerativeClusterer(Points, maxSize: Points.Count, maxRadius: Mathf.Infinity); + new AgglomerativeClusterer(Objects, maxSize: Objects.Count, maxRadius: Mathf.Infinity); clusterer.Cluster(); Assert.AreEqual(clusterer.Clusters.Count, 1); Cluster cluster = clusterer.Clusters[0]; - Assert.AreEqual(cluster.Size(), Points.Count); + Assert.AreEqual(cluster.Size(), Objects.Count); Assert.AreEqual(cluster.Centroid(), new Vector3(0, 1.25f, 0)); } [Test] public void TestMaxSizeOne() { AgglomerativeClusterer clusterer = - new AgglomerativeClusterer(Points, maxSize: 1, maxRadius: Mathf.Infinity); + new AgglomerativeClusterer(Objects, maxSize: 1, maxRadius: Mathf.Infinity); clusterer.Cluster(); - Assert.AreEqual(clusterer.Clusters.Count, Points.Count); + Assert.AreEqual(clusterer.Clusters.Count, Objects.Count); foreach (var cluster in clusterer.Clusters) { Assert.AreEqual(cluster.Size(), 1); } @@ -38,9 +44,9 @@ public void TestMaxSizeOne() { [Test] public void TestZeroRadius() { AgglomerativeClusterer clusterer = - new AgglomerativeClusterer(Points, maxSize: Points.Count, maxRadius: 0); + new AgglomerativeClusterer(Objects, maxSize: Objects.Count, maxRadius: 0); clusterer.Cluster(); - Assert.AreEqual(clusterer.Clusters.Count, Points.Count); + Assert.AreEqual(clusterer.Clusters.Count, Objects.Count); foreach (var cluster in clusterer.Clusters) { Assert.AreEqual(cluster.Size(), 1); } @@ -49,7 +55,7 @@ public void TestZeroRadius() { [Test] public void TestSmallRadius() { AgglomerativeClusterer clusterer = - new AgglomerativeClusterer(Points, maxSize: Points.Count, maxRadius: 1); + new AgglomerativeClusterer(Objects, maxSize: Objects.Count, maxRadius: 1); clusterer.Cluster(); Assert.AreEqual(clusterer.Clusters.Count, 3); List clusters = clusterer.Clusters.OrderBy(cluster => cluster.Coordinates[1]).ToList(); diff --git a/Assets/Tests/EditMode/ClusterTest.cs b/Assets/Tests/EditMode/ClusterTest.cs index 4c89d8d5..2d30f27f 100644 --- a/Assets/Tests/EditMode/ClusterTest.cs +++ b/Assets/Tests/EditMode/ClusterTest.cs @@ -5,9 +5,15 @@ using UnityEngine.TestTools; public class ClusterTest { - public static Cluster GenerateCluster(in IReadOnlyList points) { + public static GameObject GenerateObject(in Vector3 position) { + GameObject obj = new GameObject(); + obj.transform.position = position; + return obj; + } + + public static Cluster GenerateCluster(in IReadOnlyList objects) { Cluster cluster = new Cluster(); - cluster.AddPoints(points); + cluster.AddObjects(objects); cluster.Recenter(); return cluster; } @@ -15,11 +21,11 @@ public static Cluster GenerateCluster(in IReadOnlyList points) { [Test] public void TestSize() { const int size = 10; - List points = new List(); + List objects = new List(); for (int i = 0; i < size; ++i) { - points.Add(new Vector3(0, i, 0)); + objects.Add(GenerateObject(new Vector3(0, i, 0))); } - Cluster cluster = GenerateCluster(points); + Cluster cluster = GenerateCluster(objects); Assert.AreEqual(cluster.Size(), size); } @@ -29,45 +35,44 @@ public void TestIsEmpty() { Assert.IsTrue(emptyCluster.IsEmpty()); Cluster cluster = new Cluster(); - cluster.AddPoint(new Vector3(1, -1, 0)); + cluster.AddObject(new GameObject()); Assert.IsFalse(cluster.IsEmpty()); } [Test] public void TestRadius() { const float radius = 5; - List points = new List { - new Vector3(0, radius, 0), - new Vector3(0, -radius, 0), - }; - Cluster cluster = GenerateCluster(points); + List objects = new List(); + objects.Add(GenerateObject(new Vector3(0, radius, 0))); + objects.Add(GenerateObject(new Vector3(0, -radius, 0))); + Cluster cluster = GenerateCluster(objects); Assert.AreEqual(cluster.Radius(), radius); } [Test] public void TestCentroid() { const float radius = 3; - List points = new List(); + List objects = new List(); for (int i = -1; i <= 1; ++i) { for (int j = -1; j <= 1; ++j) { - points.Add(new Vector3(i, j, 0)); + objects.Add(GenerateObject(new Vector3(i, j, 0))); } } - Cluster cluster = GenerateCluster(points); + Cluster cluster = GenerateCluster(objects); Assert.AreEqual(cluster.Centroid(), Vector3.zero); } [Test] public void TestRecenter() { const float radius = 3; - List points = new List(); + List objects = new List(); for (int i = -1; i <= 1; ++i) { for (int j = -1; j <= 1; ++j) { - points.Add(new Vector3(i, j, 0)); + objects.Add(GenerateObject(new Vector3(i, j, 0))); } } - Cluster cluster = GenerateCluster(points); - cluster.AddPoint(new Vector3(10, -10, 0)); + Cluster cluster = GenerateCluster(objects); + cluster.AddObject(GenerateObject(new Vector3(10, -10, 0))); Assert.AreNotEqual(cluster.Coordinates, new Vector3(1, -1, 0)); cluster.Recenter(); Assert.AreEqual(cluster.Coordinates, new Vector3(1, -1, 0)); @@ -76,14 +81,14 @@ public void TestRecenter() { [Test] public void TestMerge() { const int size = 10; - List points1 = new List(); - List points2 = new List(); + List objects1 = new List(); + List objects2 = new List(); for (int i = 0; i < size; ++i) { - points1.Add(new Vector3(0, i, 0)); - points2.Add(new Vector3(i, 0, 0)); + objects1.Add(GenerateObject(new Vector3(0, i, 0))); + objects2.Add(GenerateObject(new Vector3(i, 0, 0))); } - Cluster cluster1 = GenerateCluster(points1); - Cluster cluster2 = GenerateCluster(points2); + Cluster cluster1 = GenerateCluster(objects1); + Cluster cluster2 = GenerateCluster(objects2); int size1 = cluster1.Size(); int size2 = cluster2.Size(); Vector3 centroid1 = cluster1.Centroid(); diff --git a/Assets/Tests/EditMode/KMeansClustererTest.cs b/Assets/Tests/EditMode/KMeansClustererTest.cs index e87b90aa..2611b9d0 100644 --- a/Assets/Tests/EditMode/KMeansClustererTest.cs +++ b/Assets/Tests/EditMode/KMeansClustererTest.cs @@ -6,49 +6,61 @@ using UnityEngine.TestTools; public class KMeansClustererTest { - public static readonly List Points = new List { - new Vector3(0, 0, 0), - new Vector3(0, 1, 0), - new Vector3(0, 1.5f, 0), - new Vector3(0, 2.5f, 0), + public static readonly List Objects = new List { + GenerateObject(new Vector3(0, 0, 0)), + GenerateObject(new Vector3(0, 1, 0)), + GenerateObject(new Vector3(0, 1.5f, 0)), + GenerateObject(new Vector3(0, 2.5f, 0)), }; + public static GameObject GenerateObject(in Vector3 position) { + GameObject obj = new GameObject(); + obj.transform.position = position; + return obj; + } + [Test] public void TestSingleCluster() { - KMeansClusterer clusterer = new KMeansClusterer(Points, k: 1); + KMeansClusterer clusterer = new KMeansClusterer(Objects, k: 1); clusterer.Cluster(); Cluster cluster = clusterer.Clusters[0]; - Assert.AreEqual(cluster.Size(), Points.Count); + Assert.AreEqual(cluster.Size(), Objects.Count); Assert.AreEqual(cluster.Coordinates, new Vector3(0, 1.25f, 0)); Assert.AreEqual(cluster.Centroid(), new Vector3(0, 1.25f, 0)); } } public class ConstrainedKMeansClustererTest { - public static readonly List Points = new List { - new Vector3(0, 0, 0), - new Vector3(0, 1, 0), - new Vector3(0, 1.5f, 0), - new Vector3(0, 2.5f, 0), + public static readonly List Objects = new List { + GenerateObject(new Vector3(0, 0, 0)), + GenerateObject(new Vector3(0, 1, 0)), + GenerateObject(new Vector3(0, 1.5f, 0)), + GenerateObject(new Vector3(0, 2.5f, 0)), }; + public static GameObject GenerateObject(in Vector3 position) { + GameObject obj = new GameObject(); + obj.transform.position = position; + return obj; + } + [Test] public void TestSingleCluster() { ConstrainedKMeansClusterer clusterer = - new ConstrainedKMeansClusterer(Points, maxSize: Points.Count, maxRadius: Mathf.Infinity); + new ConstrainedKMeansClusterer(Objects, maxSize: Objects.Count, maxRadius: Mathf.Infinity); clusterer.Cluster(); Assert.AreEqual(clusterer.Clusters.Count, 1); Cluster cluster = clusterer.Clusters[0]; - Assert.AreEqual(cluster.Size(), Points.Count); + Assert.AreEqual(cluster.Size(), Objects.Count); Assert.AreEqual(cluster.Centroid(), new Vector3(0, 1.25f, 0)); } [Test] public void TestMaxSizeOne() { ConstrainedKMeansClusterer clusterer = - new ConstrainedKMeansClusterer(Points, maxSize: 1, maxRadius: Mathf.Infinity); + new ConstrainedKMeansClusterer(Objects, maxSize: 1, maxRadius: Mathf.Infinity); clusterer.Cluster(); - Assert.AreEqual(clusterer.Clusters.Count, Points.Count); + Assert.AreEqual(clusterer.Clusters.Count, Objects.Count); foreach (var cluster in clusterer.Clusters) { Assert.AreEqual(cluster.Size(), 1); } @@ -57,9 +69,9 @@ public void TestMaxSizeOne() { [Test] public void TestZeroRadius() { ConstrainedKMeansClusterer clusterer = - new ConstrainedKMeansClusterer(Points, maxSize: Points.Count, maxRadius: 0); + new ConstrainedKMeansClusterer(Objects, maxSize: Objects.Count, maxRadius: 0); clusterer.Cluster(); - Assert.AreEqual(clusterer.Clusters.Count, Points.Count); + Assert.AreEqual(clusterer.Clusters.Count, Objects.Count); foreach (var cluster in clusterer.Clusters) { Assert.AreEqual(cluster.Size(), 1); } @@ -68,7 +80,7 @@ public void TestZeroRadius() { [Test] public void TestSmallRadius() { ConstrainedKMeansClusterer clusterer = - new ConstrainedKMeansClusterer(Points, maxSize: Points.Count, maxRadius: 1); + new ConstrainedKMeansClusterer(Objects, maxSize: Objects.Count, maxRadius: 1); clusterer.Cluster(); Assert.AreEqual(clusterer.Clusters.Count, 2); List clusters = clusterer.Clusters.OrderBy(cluster => cluster.Coordinates[1]).ToList(); From 4cb18bacb26d6dfeed34b550d6346a0d808c8bfa Mon Sep 17 00:00:00 2001 From: Daniel Lovell Date: Wed, 8 Jan 2025 17:19:38 -0800 Subject: [PATCH 09/13] Suggest new KMeansClustererTest for improper clearing of membership In my initial review I couldn't understand how the cluster membership was being cleared, so I wrote this test. Perhaps its useful to retain, despite the fact that your implementation works fine and the test passes with no modification to the algo. --- Assets/Tests/EditMode/KMeansClustererTest.cs | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/Assets/Tests/EditMode/KMeansClustererTest.cs b/Assets/Tests/EditMode/KMeansClustererTest.cs index 2611b9d0..d7ee4049 100644 --- a/Assets/Tests/EditMode/KMeansClustererTest.cs +++ b/Assets/Tests/EditMode/KMeansClustererTest.cs @@ -28,6 +28,76 @@ public void TestSingleCluster() { Assert.AreEqual(cluster.Coordinates, new Vector3(0, 1.25f, 0)); Assert.AreEqual(cluster.Centroid(), new Vector3(0, 1.25f, 0)); } + + + // Test to reveal improper clearing of cluster memberships + [Test] + public void TestTwoDistinctClustersWithResetNeeded() { + // Group A: points near (0,0,0) + var groupA = new List { + GenerateObject(new Vector3(0, 0, 0)), + GenerateObject(new Vector3(1, 0, 0)), + GenerateObject(new Vector3(0, 1, 0)), + GenerateObject(new Vector3(1, 1, 0)), + }; + + // Group B: points near (10,10,10) + var groupB = new List { + GenerateObject(new Vector3(10, 10, 10)), + GenerateObject(new Vector3(11, 10, 10)), + GenerateObject(new Vector3(10, 11, 10)), + GenerateObject(new Vector3(11, 11, 10)), + }; + + // Combine them + var objects = new List(); + objects.AddRange(groupA); + objects.AddRange(groupB); + + // Create clusterer with k=2 + KMeansClusterer clusterer = new KMeansClusterer(objects, k: 2); + clusterer.Cluster(); + + // We expect exactly 2 clusters + Assert.AreEqual(2, clusterer.Clusters.Count); + + // Retrieve the clusters + Cluster c0 = clusterer.Clusters[0]; + Cluster c1 = clusterer.Clusters[1]; + + // Because they're well separated, each cluster should contain all points + // from one group or the other, not a mixture. check via centroids. + var centroid0 = c0.Centroid(); + var centroid1 = c1.Centroid(); + + // One centroid should be near (0.5, 0.5, 0), the other near (10.5, 10.5, 10) + // Allow epsilon error. + float expectedDistanceA = Vector3.Distance(centroid0, new Vector3(0.5f, 0.5f, 0)); + float expectedDistanceB = Vector3.Distance(centroid1, new Vector3(10.5f, 10.5f, 10)); + + // It's possible the cluster indices are swapped, so check both permutations: + bool correctPlacement = + (expectedDistanceA < 1f && expectedDistanceB < 1f) + || + (Vector3.Distance(centroid0, new Vector3(10.5f, 10.5f, 10)) < 1f && + Vector3.Distance(centroid1, new Vector3(0.5f, 0.5f, 0)) < 1f); + + Assert.IsTrue(correctPlacement, + "Centroids not close to the expected group centers. Possible leftover membership from a previous iteration if clusters not cleared."); + + // Additionally, can count membership to confirm each cluster + // got exactly four points for a more direct check: + int cluster0Count = c0.Size(); + int cluster1Count = c1.Size(); + Assert.AreEqual(8, cluster0Count + cluster1Count, + "Total membership across clusters does not match the total number of objects."); + + // Even if the clusters swapped roles, each cluster should have 4 points + // if membership was properly reset and re-assigned. + bool clusterCountsValid = (cluster0Count == 4 && cluster1Count == 4); + Assert.IsTrue(clusterCountsValid, + $"Cluster sizes not as expected. c0={cluster0Count}, c1={cluster1Count}"); + } } public class ConstrainedKMeansClustererTest { From 3daa52a869a12ec59538f54b3df00dd346e54828 Mon Sep 17 00:00:00 2001 From: Daniel Lovell Date: Wed, 8 Jan 2025 17:21:06 -0800 Subject: [PATCH 10/13] Apply clang-format to new KMeansClusterer test Sorry about that --- Assets/Tests/EditMode/KMeansClustererTest.cs | 130 +++++++++---------- 1 file changed, 64 insertions(+), 66 deletions(-) diff --git a/Assets/Tests/EditMode/KMeansClustererTest.cs b/Assets/Tests/EditMode/KMeansClustererTest.cs index d7ee4049..7853df0f 100644 --- a/Assets/Tests/EditMode/KMeansClustererTest.cs +++ b/Assets/Tests/EditMode/KMeansClustererTest.cs @@ -28,75 +28,73 @@ public void TestSingleCluster() { Assert.AreEqual(cluster.Coordinates, new Vector3(0, 1.25f, 0)); Assert.AreEqual(cluster.Centroid(), new Vector3(0, 1.25f, 0)); } - - + // Test to reveal improper clearing of cluster memberships [Test] public void TestTwoDistinctClustersWithResetNeeded() { - // Group A: points near (0,0,0) - var groupA = new List { - GenerateObject(new Vector3(0, 0, 0)), - GenerateObject(new Vector3(1, 0, 0)), - GenerateObject(new Vector3(0, 1, 0)), - GenerateObject(new Vector3(1, 1, 0)), - }; - - // Group B: points near (10,10,10) - var groupB = new List { - GenerateObject(new Vector3(10, 10, 10)), - GenerateObject(new Vector3(11, 10, 10)), - GenerateObject(new Vector3(10, 11, 10)), - GenerateObject(new Vector3(11, 11, 10)), - }; - - // Combine them - var objects = new List(); - objects.AddRange(groupA); - objects.AddRange(groupB); - - // Create clusterer with k=2 - KMeansClusterer clusterer = new KMeansClusterer(objects, k: 2); - clusterer.Cluster(); - - // We expect exactly 2 clusters - Assert.AreEqual(2, clusterer.Clusters.Count); - - // Retrieve the clusters - Cluster c0 = clusterer.Clusters[0]; - Cluster c1 = clusterer.Clusters[1]; - - // Because they're well separated, each cluster should contain all points - // from one group or the other, not a mixture. check via centroids. - var centroid0 = c0.Centroid(); - var centroid1 = c1.Centroid(); - - // One centroid should be near (0.5, 0.5, 0), the other near (10.5, 10.5, 10) - // Allow epsilon error. - float expectedDistanceA = Vector3.Distance(centroid0, new Vector3(0.5f, 0.5f, 0)); - float expectedDistanceB = Vector3.Distance(centroid1, new Vector3(10.5f, 10.5f, 10)); - - // It's possible the cluster indices are swapped, so check both permutations: - bool correctPlacement = - (expectedDistanceA < 1f && expectedDistanceB < 1f) - || - (Vector3.Distance(centroid0, new Vector3(10.5f, 10.5f, 10)) < 1f && - Vector3.Distance(centroid1, new Vector3(0.5f, 0.5f, 0)) < 1f); - - Assert.IsTrue(correctPlacement, - "Centroids not close to the expected group centers. Possible leftover membership from a previous iteration if clusters not cleared."); - - // Additionally, can count membership to confirm each cluster - // got exactly four points for a more direct check: - int cluster0Count = c0.Size(); - int cluster1Count = c1.Size(); - Assert.AreEqual(8, cluster0Count + cluster1Count, - "Total membership across clusters does not match the total number of objects."); - - // Even if the clusters swapped roles, each cluster should have 4 points - // if membership was properly reset and re-assigned. - bool clusterCountsValid = (cluster0Count == 4 && cluster1Count == 4); - Assert.IsTrue(clusterCountsValid, - $"Cluster sizes not as expected. c0={cluster0Count}, c1={cluster1Count}"); + // Group A: points near (0,0,0) + var groupA = new List { + GenerateObject(new Vector3(0, 0, 0)), + GenerateObject(new Vector3(1, 0, 0)), + GenerateObject(new Vector3(0, 1, 0)), + GenerateObject(new Vector3(1, 1, 0)), + }; + + // Group B: points near (10,10,10) + var groupB = new List { + GenerateObject(new Vector3(10, 10, 10)), + GenerateObject(new Vector3(11, 10, 10)), + GenerateObject(new Vector3(10, 11, 10)), + GenerateObject(new Vector3(11, 11, 10)), + }; + + // Combine them + var objects = new List(); + objects.AddRange(groupA); + objects.AddRange(groupB); + + // Create clusterer with k=2 + KMeansClusterer clusterer = new KMeansClusterer(objects, k: 2); + clusterer.Cluster(); + + // We expect exactly 2 clusters + Assert.AreEqual(2, clusterer.Clusters.Count); + + // Retrieve the clusters + Cluster c0 = clusterer.Clusters[0]; + Cluster c1 = clusterer.Clusters[1]; + + // Because they're well separated, each cluster should contain all points + // from one group or the other, not a mixture. check via centroids. + var centroid0 = c0.Centroid(); + var centroid1 = c1.Centroid(); + + // One centroid should be near (0.5, 0.5, 0), the other near (10.5, 10.5, 10) + // Allow epsilon error. + float expectedDistanceA = Vector3.Distance(centroid0, new Vector3(0.5f, 0.5f, 0)); + float expectedDistanceB = Vector3.Distance(centroid1, new Vector3(10.5f, 10.5f, 10)); + + // It's possible the cluster indices are swapped, so check both permutations: + bool correctPlacement = (expectedDistanceA < 1f && expectedDistanceB < 1f) || + (Vector3.Distance(centroid0, new Vector3(10.5f, 10.5f, 10)) < 1f && + Vector3.Distance(centroid1, new Vector3(0.5f, 0.5f, 0)) < 1f); + + Assert.IsTrue( + correctPlacement, + "Centroids not close to the expected group centers. Possible leftover membership from a previous iteration if clusters not cleared."); + + // Additionally, can count membership to confirm each cluster + // got exactly four points for a more direct check: + int cluster0Count = c0.Size(); + int cluster1Count = c1.Size(); + Assert.AreEqual(8, cluster0Count + cluster1Count, + "Total membership across clusters does not match the total number of objects."); + + // Even if the clusters swapped roles, each cluster should have 4 points + // if membership was properly reset and re-assigned. + bool clusterCountsValid = (cluster0Count == 4 && cluster1Count == 4); + Assert.IsTrue(clusterCountsValid, + $"Cluster sizes not as expected. c0={cluster0Count}, c1={cluster1Count}"); } } From 10689cc5110798a99d4d2fbacdcdf2b6e2d7b087 Mon Sep 17 00:00:00 2001 From: Titan Yuan Date: Wed, 8 Jan 2025 17:28:30 -0800 Subject: [PATCH 11/13] Fix tests and address comments --- Assets/Tests/EditMode/ClusterTest.cs | 2 - Assets/Tests/EditMode/KMeansClustererTest.cs | 45 +++++++++----------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/Assets/Tests/EditMode/ClusterTest.cs b/Assets/Tests/EditMode/ClusterTest.cs index 2d30f27f..5279f2eb 100644 --- a/Assets/Tests/EditMode/ClusterTest.cs +++ b/Assets/Tests/EditMode/ClusterTest.cs @@ -51,7 +51,6 @@ public void TestRadius() { [Test] public void TestCentroid() { - const float radius = 3; List objects = new List(); for (int i = -1; i <= 1; ++i) { for (int j = -1; j <= 1; ++j) { @@ -64,7 +63,6 @@ public void TestCentroid() { [Test] public void TestRecenter() { - const float radius = 3; List objects = new List(); for (int i = -1; i <= 1; ++i) { for (int j = -1; j <= 1; ++j) { diff --git a/Assets/Tests/EditMode/KMeansClustererTest.cs b/Assets/Tests/EditMode/KMeansClustererTest.cs index 7853df0f..90b45092 100644 --- a/Assets/Tests/EditMode/KMeansClustererTest.cs +++ b/Assets/Tests/EditMode/KMeansClustererTest.cs @@ -29,10 +29,10 @@ public void TestSingleCluster() { Assert.AreEqual(cluster.Centroid(), new Vector3(0, 1.25f, 0)); } - // Test to reveal improper clearing of cluster memberships + // Test to reveal improper clearing of cluster memberships. [Test] public void TestTwoDistinctClustersWithResetNeeded() { - // Group A: points near (0,0,0) + // Group A: points near (0, 0, 0). var groupA = new List { GenerateObject(new Vector3(0, 0, 0)), GenerateObject(new Vector3(1, 0, 0)), @@ -40,7 +40,7 @@ public void TestTwoDistinctClustersWithResetNeeded() { GenerateObject(new Vector3(1, 1, 0)), }; - // Group B: points near (10,10,10) + // Group B: points near (10, 10, 10). var groupB = new List { GenerateObject(new Vector3(10, 10, 10)), GenerateObject(new Vector3(11, 10, 10)), @@ -48,53 +48,48 @@ public void TestTwoDistinctClustersWithResetNeeded() { GenerateObject(new Vector3(11, 11, 10)), }; - // Combine them + // Combine them. var objects = new List(); objects.AddRange(groupA); objects.AddRange(groupB); - // Create clusterer with k=2 + // Create clusterer with k = 2. KMeansClusterer clusterer = new KMeansClusterer(objects, k: 2); clusterer.Cluster(); - // We expect exactly 2 clusters + // We expect exactly 2 clusters. Assert.AreEqual(2, clusterer.Clusters.Count); - // Retrieve the clusters + // Retrieve the clusters. Cluster c0 = clusterer.Clusters[0]; Cluster c1 = clusterer.Clusters[1]; - // Because they're well separated, each cluster should contain all points - // from one group or the other, not a mixture. check via centroids. + // Because the clusters are well-separated, each cluster should contain all points from one + // group or the other, not a mixture. Check via centroids. var centroid0 = c0.Centroid(); var centroid1 = c1.Centroid(); - // One centroid should be near (0.5, 0.5, 0), the other near (10.5, 10.5, 10) - // Allow epsilon error. - float expectedDistanceA = Vector3.Distance(centroid0, new Vector3(0.5f, 0.5f, 0)); - float expectedDistanceB = Vector3.Distance(centroid1, new Vector3(10.5f, 10.5f, 10)); - - // It's possible the cluster indices are swapped, so check both permutations: - bool correctPlacement = (expectedDistanceA < 1f && expectedDistanceB < 1f) || - (Vector3.Distance(centroid0, new Vector3(10.5f, 10.5f, 10)) < 1f && - Vector3.Distance(centroid1, new Vector3(0.5f, 0.5f, 0)) < 1f); - + // One centroid should be near (0.5, 0.5, 0), the other near (10.5, 10.5, 10). + var expectedCentroid0 = new Vector3(0.5f, 0.5f, 0); + var expectedCentroid1 = new Vector3(10.5f, 10.5f, 10); + bool correctPlacement = (centroid0 == expectedCentroid0 && centroid1 == expectedCentroid1) || + (centroid0 == expectedCentroid1 && centroid1 == expectedCentroid0); Assert.IsTrue( correctPlacement, "Centroids not close to the expected group centers. Possible leftover membership from a previous iteration if clusters not cleared."); - // Additionally, can count membership to confirm each cluster - // got exactly four points for a more direct check: + // Additionally, we can count membership to confirm that each cluster got exactly four points + // for a more direct check. int cluster0Count = c0.Size(); int cluster1Count = c1.Size(); Assert.AreEqual(8, cluster0Count + cluster1Count, "Total membership across clusters does not match the total number of objects."); - // Even if the clusters swapped roles, each cluster should have 4 points - // if membership was properly reset and re-assigned. - bool clusterCountsValid = (cluster0Count == 4 && cluster1Count == 4); + // Even if the clusters swapped roles, each cluster should have 4 points if membership was + // properly reset and re-assigned. + bool clusterCountsValid = cluster0Count == 4 && cluster1Count == 4; Assert.IsTrue(clusterCountsValid, - $"Cluster sizes not as expected. c0={cluster0Count}, c1={cluster1Count}"); + $"Cluster sizes not as expected. c0={cluster0Count}, c1={cluster1Count}."); } } From 9dfb68eefc2bb51daede6e9f7adf8313bd141ec8 Mon Sep 17 00:00:00 2001 From: Titan Yuan Date: Wed, 8 Jan 2025 18:56:43 -0800 Subject: [PATCH 12/13] Fix assert order --- .../EditMode/AgglomerativeClustererTest.cs | 28 +++++++++---------- Assets/Tests/EditMode/ClusterTest.cs | 14 +++++----- Assets/Tests/EditMode/KMeansClustererTest.cs | 23 ++++++++------- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/Assets/Tests/EditMode/AgglomerativeClustererTest.cs b/Assets/Tests/EditMode/AgglomerativeClustererTest.cs index 8ba24b24..22e6c659 100644 --- a/Assets/Tests/EditMode/AgglomerativeClustererTest.cs +++ b/Assets/Tests/EditMode/AgglomerativeClustererTest.cs @@ -24,10 +24,10 @@ public void TestSingleCluster() { AgglomerativeClusterer clusterer = new AgglomerativeClusterer(Objects, maxSize: Objects.Count, maxRadius: Mathf.Infinity); clusterer.Cluster(); - Assert.AreEqual(clusterer.Clusters.Count, 1); + Assert.AreEqual(1, clusterer.Clusters.Count); Cluster cluster = clusterer.Clusters[0]; - Assert.AreEqual(cluster.Size(), Objects.Count); - Assert.AreEqual(cluster.Centroid(), new Vector3(0, 1.25f, 0)); + Assert.AreEqual(Objects.Count, cluster.Size()); + Assert.AreEqual(new Vector3(0, 1.25f, 0), cluster.Centroid()); } [Test] @@ -35,9 +35,9 @@ public void TestMaxSizeOne() { AgglomerativeClusterer clusterer = new AgglomerativeClusterer(Objects, maxSize: 1, maxRadius: Mathf.Infinity); clusterer.Cluster(); - Assert.AreEqual(clusterer.Clusters.Count, Objects.Count); + Assert.AreEqual(Objects.Count, clusterer.Clusters.Count); foreach (var cluster in clusterer.Clusters) { - Assert.AreEqual(cluster.Size(), 1); + Assert.AreEqual(1, cluster.Size()); } } @@ -46,9 +46,9 @@ public void TestZeroRadius() { AgglomerativeClusterer clusterer = new AgglomerativeClusterer(Objects, maxSize: Objects.Count, maxRadius: 0); clusterer.Cluster(); - Assert.AreEqual(clusterer.Clusters.Count, Objects.Count); + Assert.AreEqual(Objects.Count, clusterer.Clusters.Count); foreach (var cluster in clusterer.Clusters) { - Assert.AreEqual(cluster.Size(), 1); + Assert.AreEqual(1, cluster.Size()); } } @@ -57,13 +57,13 @@ public void TestSmallRadius() { AgglomerativeClusterer clusterer = new AgglomerativeClusterer(Objects, maxSize: Objects.Count, maxRadius: 1); clusterer.Cluster(); - Assert.AreEqual(clusterer.Clusters.Count, 3); + Assert.AreEqual(3, clusterer.Clusters.Count); List clusters = clusterer.Clusters.OrderBy(cluster => cluster.Coordinates[1]).ToList(); - Assert.AreEqual(clusters[0].Size(), 1); - Assert.AreEqual(clusters[0].Coordinates, new Vector3(0, 0, 0)); - Assert.AreEqual(clusters[1].Size(), 2); - Assert.AreEqual(clusters[1].Coordinates, new Vector3(0, 1.25f, 0)); - Assert.AreEqual(clusters[2].Size(), 1); - Assert.AreEqual(clusters[2].Coordinates, new Vector3(0, 2.5f, 0)); + Assert.AreEqual(1, clusters[0].Size()); + Assert.AreEqual(new Vector3(0, 0, 0), clusters[0].Coordinates); + Assert.AreEqual(2, clusters[1].Size()); + Assert.AreEqual(new Vector3(0, 1.25f, 0), clusters[1].Coordinates); + Assert.AreEqual(1, clusters[2].Size()); + Assert.AreEqual(new Vector3(0, 2.5f, 0), clusters[2].Coordinates); } } diff --git a/Assets/Tests/EditMode/ClusterTest.cs b/Assets/Tests/EditMode/ClusterTest.cs index 5279f2eb..24eb851a 100644 --- a/Assets/Tests/EditMode/ClusterTest.cs +++ b/Assets/Tests/EditMode/ClusterTest.cs @@ -26,7 +26,7 @@ public void TestSize() { objects.Add(GenerateObject(new Vector3(0, i, 0))); } Cluster cluster = GenerateCluster(objects); - Assert.AreEqual(cluster.Size(), size); + Assert.AreEqual(size, cluster.Size()); } [Test] @@ -46,7 +46,7 @@ public void TestRadius() { objects.Add(GenerateObject(new Vector3(0, radius, 0))); objects.Add(GenerateObject(new Vector3(0, -radius, 0))); Cluster cluster = GenerateCluster(objects); - Assert.AreEqual(cluster.Radius(), radius); + Assert.AreEqual(size, cluster.Radius()); } [Test] @@ -58,7 +58,7 @@ public void TestCentroid() { } } Cluster cluster = GenerateCluster(objects); - Assert.AreEqual(cluster.Centroid(), Vector3.zero); + Assert.AreEqual(Vector3.zero, cluster.Centroid()); } [Test] @@ -71,9 +71,9 @@ public void TestRecenter() { } Cluster cluster = GenerateCluster(objects); cluster.AddObject(GenerateObject(new Vector3(10, -10, 0))); - Assert.AreNotEqual(cluster.Coordinates, new Vector3(1, -1, 0)); + Assert.AreNotEqual(new Vector3(1, -1, 0), cluster.Coordinates); cluster.Recenter(); - Assert.AreEqual(cluster.Coordinates, new Vector3(1, -1, 0)); + Assert.AreEqual(new Vector3(1, -1, 0), cluster.Coordinates); } [Test] @@ -93,7 +93,7 @@ public void TestMerge() { Vector3 centroid2 = cluster2.Centroid(); cluster1.Merge(cluster2); cluster1.Recenter(); - Assert.AreEqual(cluster1.Size(), size1 + size2); - Assert.AreEqual(cluster1.Coordinates, (centroid1 + centroid2) / 2); + Assert.AreEqual(size1 + size2, cluster1.Size()); + Assert.AreEqual((centroid1 + centroid2) / 2, cluster1.Coordinates); } } diff --git a/Assets/Tests/EditMode/KMeansClustererTest.cs b/Assets/Tests/EditMode/KMeansClustererTest.cs index 90b45092..fe51ac91 100644 --- a/Assets/Tests/EditMode/KMeansClustererTest.cs +++ b/Assets/Tests/EditMode/KMeansClustererTest.cs @@ -24,9 +24,9 @@ public void TestSingleCluster() { KMeansClusterer clusterer = new KMeansClusterer(Objects, k: 1); clusterer.Cluster(); Cluster cluster = clusterer.Clusters[0]; - Assert.AreEqual(cluster.Size(), Objects.Count); - Assert.AreEqual(cluster.Coordinates, new Vector3(0, 1.25f, 0)); - Assert.AreEqual(cluster.Centroid(), new Vector3(0, 1.25f, 0)); + Assert.AreEqual(Objects.Count, cluster.Size()); + Assert.AreEqual(new Vector3(0, 1.25f, 0), cluster.Coordinates); + Assert.AreEqual(new Vector3(0, 1.25f, 0), cluster.Centroid()); } // Test to reveal improper clearing of cluster memberships. @@ -112,10 +112,10 @@ public void TestSingleCluster() { ConstrainedKMeansClusterer clusterer = new ConstrainedKMeansClusterer(Objects, maxSize: Objects.Count, maxRadius: Mathf.Infinity); clusterer.Cluster(); - Assert.AreEqual(clusterer.Clusters.Count, 1); + Assert.AreEqual(1, clusterer.Clusters.Count); Cluster cluster = clusterer.Clusters[0]; - Assert.AreEqual(cluster.Size(), Objects.Count); - Assert.AreEqual(cluster.Centroid(), new Vector3(0, 1.25f, 0)); + Assert.AreEqual(Objects.Count, cluster.Size()); + Assert.AreEqual(new Vector3(0, 1.25f, 0), cluster.Centroid()); } [Test] @@ -123,9 +123,9 @@ public void TestMaxSizeOne() { ConstrainedKMeansClusterer clusterer = new ConstrainedKMeansClusterer(Objects, maxSize: 1, maxRadius: Mathf.Infinity); clusterer.Cluster(); - Assert.AreEqual(clusterer.Clusters.Count, Objects.Count); + Assert.AreEqual(Objects.Count, clusterer.Clusters.Count); foreach (var cluster in clusterer.Clusters) { - Assert.AreEqual(cluster.Size(), 1); + Assert.AreEqual(1, cluster.Size()); } } @@ -134,9 +134,9 @@ public void TestZeroRadius() { ConstrainedKMeansClusterer clusterer = new ConstrainedKMeansClusterer(Objects, maxSize: Objects.Count, maxRadius: 0); clusterer.Cluster(); - Assert.AreEqual(clusterer.Clusters.Count, Objects.Count); + Assert.AreEqual(Objects.Count, clusterer.Clusters.Count); foreach (var cluster in clusterer.Clusters) { - Assert.AreEqual(cluster.Size(), 1); + Assert.AreEqual(1, cluster.Size()); } } @@ -145,7 +145,6 @@ public void TestSmallRadius() { ConstrainedKMeansClusterer clusterer = new ConstrainedKMeansClusterer(Objects, maxSize: Objects.Count, maxRadius: 1); clusterer.Cluster(); - Assert.AreEqual(clusterer.Clusters.Count, 2); - List clusters = clusterer.Clusters.OrderBy(cluster => cluster.Coordinates[1]).ToList(); + Assert.AreEqual(2, clusterer.Clusters.Count); } } From 52c6191af8d879ec20674444941975a3f30838c3 Mon Sep 17 00:00:00 2001 From: Titan Yuan Date: Wed, 8 Jan 2025 19:05:20 -0800 Subject: [PATCH 13/13] Fix typo in cluster test --- Assets/Tests/EditMode/ClusterTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Tests/EditMode/ClusterTest.cs b/Assets/Tests/EditMode/ClusterTest.cs index 24eb851a..d594ff24 100644 --- a/Assets/Tests/EditMode/ClusterTest.cs +++ b/Assets/Tests/EditMode/ClusterTest.cs @@ -46,7 +46,7 @@ public void TestRadius() { objects.Add(GenerateObject(new Vector3(0, radius, 0))); objects.Add(GenerateObject(new Vector3(0, -radius, 0))); Cluster cluster = GenerateCluster(objects); - Assert.AreEqual(size, cluster.Radius()); + Assert.AreEqual(radius, cluster.Radius()); } [Test]