namespace RobotNet10.RobotApp.Detection; /// /// OPTICS (Ordering Points To Identify the Clustering Structure) clustering algorithm /// Implements density-based clustering with spatial indexing optimizations /// public class OpticsClusteringAlgorithm(double eps, int minPts) { private readonly List _points = []; private readonly List _orderedList = []; private readonly Dictionary> _grid = []; private double _gridCellSize = 0; private KDTree? _kdTree; /// /// KD-tree implementation for efficient radius searches /// private class KDTree { private class Node { public int Idx { get; set; } public int Left { get; set; } = -1; public int Right { get; set; } = -1; } private readonly List _pts; private readonly List _nodes; private readonly int _root; public KDTree(List points) { _pts = points; _nodes = []; _root = -1; if (points.Count > 0) { var idxs = Enumerable.Range(0, points.Count).ToList(); _root = BuildRec(idxs, 0, idxs.Count - 1, 0); } } private int BuildRec(List idxs, int l, int r, int depth) { if (l > r) return -1; int axis = depth % 2; int m = (l + r) / 2; // Partition based on axis idxs.Sort(l, r - l + 1, Comparer.Create((a, b) => { if (axis == 0) return _pts[a].X.CompareTo(_pts[b].X); return _pts[a].Y.CompareTo(_pts[b].Y); })); int nodeIdx = _nodes.Count; _nodes.Add(new Node { Idx = idxs[m] }); _nodes[nodeIdx].Left = BuildRec(idxs, l, m - 1, depth + 1); _nodes[nodeIdx].Right = BuildRec(idxs, m + 1, r, depth + 1); return nodeIdx; } public List RadiusSearch(Point q, double radius) { var result = new List(); double r2 = radius * radius; SearchRec(_root, q, r2, 0, result); return result; } private void SearchRec(int nodeIdx, Point q, double r2, int depth, List output) { if (nodeIdx < 0) return; var node = _nodes[nodeIdx]; var p = _pts[node.Idx]; double dx = q.X - p.X; double dy = q.Y - p.Y; double dist2 = dx * dx + dy * dy; if (dist2 <= r2) output.Add(node.Idx); int axis = depth % 2; double diff = axis == 0 ? dx : dy; if (diff <= 0) { SearchRec(node.Left, q, r2, depth + 1, output); if (diff * diff <= r2) SearchRec(node.Right, q, r2, depth + 1, output); } else { SearchRec(node.Right, q, r2, depth + 1, output); if (diff * diff <= r2) SearchRec(node.Left, q, r2, depth + 1, output); } } } /// /// Add a single point to the dataset /// public void AddPoint(double x, double y, double range, double alpha) { _points.Add(new Point(x, y, range, alpha)); // If grid is enabled, insert into the grid if (_gridCellSize > 0) { int ix = (int)Math.Floor(x / _gridCellSize); int iy = (int)Math.Floor(y / _gridCellSize); long key = CellKey(ix, iy); if (!_grid.ContainsKey(key)) _grid[key] = []; _grid[key].Add(_points.Count - 1); } } /// /// Add multiple points to the dataset /// public void AddPoints(List newPoints) { _points.Clear(); _points.AddRange(newPoints); // Build spatial index using eps as cell size _grid.Clear(); _gridCellSize = eps; BuildSpatialIndex(); BuildKDTree(); } /// /// Clear all points and reset the algorithm state /// public void ClearPoints() { _points.Clear(); _orderedList.Clear(); _grid.Clear(); _gridCellSize = 0; _kdTree = null; } /// /// Run the OPTICS clustering algorithm /// public void Run() { _orderedList.Clear(); _orderedList.Capacity = _points.Count; for (int i = 0; i < _points.Count; i++) { if (!_points[i].Processed) { ExpandClusterOrder(i); } } } /// /// Get the ordered list of point indices after running OPTICS /// public IReadOnlyList GetClusterOrder() => _orderedList.AsReadOnly(); /// /// Extract clusters using a reachability distance threshold /// public List> ExtractClusters(double clusterThreshold) { var clusters = new List>(); var currentCluster = new List(); double thrSq = clusterThreshold * clusterThreshold; for (int i = 0; i < _orderedList.Count; i++) { int pointIdx = _orderedList[i]; if (_points[pointIdx].ReachabilityDistance > thrSq) { if (currentCluster.Count > 0) { clusters.Add(currentCluster); currentCluster = []; } } currentCluster.Add(pointIdx); } if (currentCluster.Count > 0) { clusters.Add(currentCluster); } return clusters; } /// /// Get clustered points as Point2D structures /// public List> GetClusters(double clusterThreshold) { var result = new List>(); var clusters = ExtractClusters(clusterThreshold); foreach (var cluster in clusters) { var clusteredPoints = new List(); foreach (int pointIdx in cluster) { var pt = _points[pointIdx]; clusteredPoints.Add(new Point2D(pt.X, pt.Y)); } result.Add(clusteredPoints); } return result; } private void ExpandClusterOrder(int pointIdx) { var neighbors = GetNeighbors(pointIdx); _points[pointIdx].Processed = true; _orderedList.Add(pointIdx); if (neighbors.Count >= minPts) { // Compute core distance for point_idx double coreDistPoint = double.MaxValue; if (neighbors.Count >= minPts) { var tmp = new List<(double, int)>(neighbors); tmp.Sort((a, b) => a.Item1.CompareTo(b.Item1)); coreDistPoint = tmp[minPts - 1].Item1; } // Priority queue ordered by reachability distance var seeds = new SortedSet<(double, int)>(Comparer<(double, int)>.Create((a, b) => { int cmp = a.Item1.CompareTo(b.Item1); return cmp != 0 ? cmp : a.Item2.CompareTo(b.Item2); })); foreach (var (dist, neighborIdx) in neighbors) { if (!_points[neighborIdx].Processed) { double newReachDist = Math.Max(dist, coreDistPoint); if (_points[neighborIdx].ReachabilityDistance == double.MaxValue) { _points[neighborIdx].ReachabilityDistance = newReachDist; seeds.Add((newReachDist, neighborIdx)); } else if (newReachDist < _points[neighborIdx].ReachabilityDistance) { _points[neighborIdx].ReachabilityDistance = newReachDist; seeds.Add((newReachDist, neighborIdx)); } } } while (seeds.Count > 0) { var (_, current) = seeds.Min; seeds.Remove(seeds.Min); var currentNeighbors = GetNeighbors(current); _points[current].Processed = true; _orderedList.Add(current); if (currentNeighbors.Count >= minPts) { // Compute core distance for current double coreDistCurrent = double.MaxValue; if (currentNeighbors.Count >= minPts) { var tmp2 = new List<(double, int)>(currentNeighbors); tmp2.Sort((a, b) => a.Item1.CompareTo(b.Item1)); coreDistCurrent = tmp2[minPts - 1].Item1; } foreach (var (dist, neighborIdx) in currentNeighbors) { if (!_points[neighborIdx].Processed) { double newReachDist = Math.Max(dist, coreDistCurrent); if (_points[neighborIdx].ReachabilityDistance == double.MaxValue) { _points[neighborIdx].ReachabilityDistance = newReachDist; seeds.Add((newReachDist, neighborIdx)); } else if (newReachDist < _points[neighborIdx].ReachabilityDistance) { _points[neighborIdx].ReachabilityDistance = newReachDist; seeds.Add((newReachDist, neighborIdx)); } } } } } } } private List<(double, int)> GetNeighbors(int pointIdx) { var neighbors = new List<(double, int)>(32); // If we have a KD-tree, prefer it for radius queries if (_kdTree != null) { var ids = _kdTree.RadiusSearch(_points[pointIdx], eps); foreach (int idx in ids) { if (idx == pointIdx) continue; double distanceSq = EuclideanDistance(_points[pointIdx], _points[idx]); neighbors.Add((distanceSq, idx)); } return neighbors; } if (_gridCellSize <= 0 || _grid.Count == 0) { // Fallback to brute-force for (int i = 0; i < _points.Count; i++) { if (i == pointIdx) continue; double distanceSq = EuclideanDistance(_points[pointIdx], _points[i]); if (distanceSq <= eps * eps) { neighbors.Add((distanceSq, i)); } } return neighbors; } var p = _points[pointIdx]; int cx = (int)Math.Floor(p.X / _gridCellSize); int cy = (int)Math.Floor(p.Y / _gridCellSize); // Search neighbor cells around (cx, cy) for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { long key = CellKey(cx + dx, cy + dy); if (_grid.TryGetValue(key, out var cellPoints)) { foreach (int idx in cellPoints) { if (idx == pointIdx) continue; double distanceSq = EuclideanDistance(p, _points[idx]); if (distanceSq <= eps * eps) { neighbors.Add((distanceSq, idx)); } } } } } return neighbors; } private static double EuclideanDistance(Point p1, Point p2) { double dx = p1.X - p2.X; double dy = p1.Y - p2.Y; return dx * dx + dy * dy; // Returns squared distance } private void BuildSpatialIndex() { if (_gridCellSize <= 0) return; _grid.Clear(); for (int i = 0; i < _points.Count; i++) { int ix = (int)Math.Floor(_points[i].X / _gridCellSize); int iy = (int)Math.Floor(_points[i].Y / _gridCellSize); long key = CellKey(ix, iy); if (!_grid.ContainsKey(key)) _grid[key] = []; _grid[key].Add(i); } } private void BuildKDTree() { if (_points.Count == 0) { _kdTree = null; return; } _kdTree = new KDTree(_points); } private static long CellKey(int ix, int iy) { // Pack two 32-bit ints into one 64-bit key return ((long)ix << 32) ^ (uint)iy; } }