From 12c0766d944a860424de8dd3f1f5ecd216980a03 Mon Sep 17 00:00:00 2001 From: Viktor Yudov Date: Fri, 16 Jun 2023 02:53:28 +0300 Subject: [PATCH 1/3] Implement DBScan algorithm --- src/visualizers/dbscan/dbscan.tsx | 98 +++++++++++++++++++++++++++++++ src/visualizers/dbscan/render.tsx | 11 ++++ src/visualizers/dbscan/start.tsx | 18 ++++++ 3 files changed, 127 insertions(+) create mode 100644 src/visualizers/dbscan/dbscan.tsx create mode 100644 src/visualizers/dbscan/render.tsx create mode 100644 src/visualizers/dbscan/start.tsx diff --git a/src/visualizers/dbscan/dbscan.tsx b/src/visualizers/dbscan/dbscan.tsx new file mode 100644 index 0000000..0c037b5 --- /dev/null +++ b/src/visualizers/dbscan/dbscan.tsx @@ -0,0 +1,98 @@ +import { bind, here } from "../../lib"; + +export const dbscan = async (points: number[][], eps: number, min_samples: number) => { + let labels = new Array(points.length).fill(0); + let cluster = new Mutable(1); + + bind("points", points); + bind("labels", labels); + bind("cluster", cluster); + + await here("start"); + let neighbors = findNeighbors(points, eps); + for (let i = 0; i < points.length; i++) { + if (await expandCluster(labels, neighbors, i, cluster, min_samples)) { + await here("new_cluster", cluster.value); + cluster.value++; + } + } + await here("done"); +} + +const expandCluster = async ( + labels: number[], + neighbors: number[][], + i: number, + cluster: Mutable, + min_samples: number +): Promise => { + if (labels[i] !== 0) { + return false; + } + + if (neighbors[i].length < min_samples) { + await here("noise", i, -1, neighbors[i].length); + labels[i] = -1; + return false; + } + + await here("expand_cluster", i, cluster.value, neighbors[i].length); + labels[i] = cluster.value; + for (let neighbor of neighbors[i]) { + if (labels[neighbor] === 0) { + await expandCluster(labels, neighbors, neighbor, cluster, min_samples); + } + } + + return true; +} + +const findNeighbors = (points: number[][], eps: number): number[][] => { + let neighbors = new Array(points.length).fill(null).map(() => []); + for (let i = 0; i < points.length - 1; i++) { + for (let j = i + 1; j < points.length; j++) { + if (distance(points[i], points[j]) < eps) { + neighbors[i].push(j); + neighbors[j].push(i); + } + } + } + return neighbors; +} + +const distance = (p1: number[], p2: number[]): number => + Math.sqrt(p1.reduce((acc, cur, i) => acc + Math.pow(cur - p2[i], 2), 0)); + +// TODO: move to lib? +class Mutable { + constructor(public value: T) { + this.value = value; + } +} + +export type DBScanState = { + cluster: Mutable; + labels: number[]; + points: number[][]; +} + +export type DBScanEvent = Start | Done | NewCluster | ExpandCluster | Noise; + +type Start = { name: "start"; } +type Done = { name: "done";} +type NewCluster = { + name: "new_cluster"; + cluster: number; +} +type ExpandCluster = { + name: "expand_cluster"; + point: number; + neighbors_count: number; +} +type Noise = { + name: "noise"; + point: number; + neighbors_count: number; +} + +export type DBScanArguments = [number[][], number, number]; diff --git a/src/visualizers/dbscan/render.tsx b/src/visualizers/dbscan/render.tsx new file mode 100644 index 0000000..53dcc86 --- /dev/null +++ b/src/visualizers/dbscan/render.tsx @@ -0,0 +1,11 @@ +import { DBScanEvent, DBScanState } from "./dbscan"; + +export type RenderProps = { + curState: DBScanState; + curEvent: DBScanEvent; +} + +export const DBScanRender = ({ curState, curEvent }) => { + console.log("Rendering", curState, curEvent); + return
TODO: render
; +} \ No newline at end of file diff --git a/src/visualizers/dbscan/start.tsx b/src/visualizers/dbscan/start.tsx new file mode 100644 index 0000000..9498d31 --- /dev/null +++ b/src/visualizers/dbscan/start.tsx @@ -0,0 +1,18 @@ +import { DBScanArguments } from "./dbscan" + +type Props = { + doStart: (args: DBScanArguments, noStop: boolean) => void +} + +const args: DBScanArguments = [ + [[0, 0], [1, 0], [0, 1], [1, 1], [10, 3], [5, 5], [5, 6], [6, 5], [6, 6]], + 2, + 3, +]; + +export const DBScanStarter = ({ doStart }: Props) => { + return
+ + +
; +} From 4f595e6d446ea77fba474ff26fb9880d767e5abd Mon Sep 17 00:00:00 2001 From: Viktor Yudov Date: Sat, 17 Jun 2023 01:17:50 +0300 Subject: [PATCH 2/3] Add index.tsx to dbscan --- src/visualizers/dbscan/dbscan.tsx | 31 +++++++++++++++++-------------- src/visualizers/dbscan/index.tsx | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 src/visualizers/dbscan/index.tsx diff --git a/src/visualizers/dbscan/dbscan.tsx b/src/visualizers/dbscan/dbscan.tsx index 0c037b5..cc807c0 100644 --- a/src/visualizers/dbscan/dbscan.tsx +++ b/src/visualizers/dbscan/dbscan.tsx @@ -1,4 +1,4 @@ -import { bind, here } from "../../lib"; +import { bind, here } from "./"; export const dbscan = async (points: number[][], eps: number, min_samples: number) => { let labels = new Array(points.length).fill(0); @@ -12,8 +12,8 @@ export const dbscan = async (points: number[][], eps: number, min_samples: numbe let neighbors = findNeighbors(points, eps); for (let i = 0; i < points.length; i++) { if (await expandCluster(labels, neighbors, i, cluster, min_samples)) { - await here("new_cluster", cluster.value); cluster.value++; + await here("new_cluster", cluster.value); } } await here("done"); @@ -31,13 +31,13 @@ const expandCluster = async ( } if (neighbors[i].length < min_samples) { - await here("noise", i, -1, neighbors[i].length); labels[i] = -1; + await here("noise", i, neighbors[i].length); return false; } - await here("expand_cluster", i, cluster.value, neighbors[i].length); labels[i] = cluster.value; + await here("expand_cluster", i, neighbors[i].length); for (let neighbor of neighbors[i]) { if (labels[neighbor] === 0) { await expandCluster(labels, neighbors, neighbor, cluster, min_samples); @@ -65,9 +65,7 @@ const distance = (p1: number[], p2: number[]): number => // TODO: move to lib? class Mutable { - constructor(public value: T) { - this.value = value; - } + constructor(public value: T) {} } export type DBScanState = { @@ -78,21 +76,26 @@ export type DBScanState = { export type DBScanEvent = Start | Done | NewCluster | ExpandCluster | Noise; -type Start = { name: "start"; } -type Done = { name: "done";} +// TODO: move Start and Done to lib? +type Start = { + name: "start", + args: [], +} +type Done = { + name: "done", + args: [], +} type NewCluster = { name: "new_cluster"; - cluster: number; + args: [cluster: number]; } type ExpandCluster = { name: "expand_cluster"; - point: number; - neighbors_count: number; + args: [point: number, neighbors_count: number]; } type Noise = { name: "noise"; - point: number; - neighbors_count: number; + args: [point: number, neighbors_count: number]; } export type DBScanArguments = [number[][], number, number]; diff --git a/src/visualizers/dbscan/index.tsx b/src/visualizers/dbscan/index.tsx new file mode 100644 index 0000000..69d6655 --- /dev/null +++ b/src/visualizers/dbscan/index.tsx @@ -0,0 +1,21 @@ +import { AlgorithmManifest } from "../../lib/manifest"; +import { RuntimeStore } from "../../lib/store"; +import { DBScanArguments, DBScanEvent, DBScanState, dbscan } from "./dbscan"; +import { DBScanRender } from "./render"; +import { DBScanStarter } from "./start"; + +export const manifest: AlgorithmManifest = { + algo: dbscan, + startComponent: DBScanStarter, + renderComponent: DBScanRender +} + +export const globalStore = new RuntimeStore(dbscan); + +export const bind = (name: keyof DBScanState, value: DBScanState[keyof DBScanState]) => { + globalStore.bind(name, value); +} + +export const here = async (name: DBScanEvent["name"], ...args: DBScanEvent["args"]): Promise => { + return globalStore.here(name, ...args); +} From 2898601d9f43c44b12c0e2cb21b4a05c79d92560 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 20 Jun 2023 01:23:19 +0300 Subject: [PATCH 3/3] Tie to main app --- src/App.tsx | 8 ++++---- src/lib/hooks.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 112cdea..e0fb978 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ -import { BubbleSortStarter } from "./visualizers/bubble-sort/start"; -import { BubbleSortRender } from "./visualizers/bubble-sort/render"; +import { DBScanStarter } from "./visualizers/dbscan/start"; +import { DBScanRender } from "./visualizers/dbscan/render"; import { useVisualizer } from "./lib/hooks"; import { useEffect, useState } from "react"; @@ -48,8 +48,8 @@ const App = () => { Use arrow keys to navigate - - + +
{events && events.map((x, i) => { return
diff --git a/src/lib/hooks.tsx b/src/lib/hooks.tsx index 8810f14..626650f 100644 --- a/src/lib/hooks.tsx +++ b/src/lib/hooks.tsx @@ -1,5 +1,5 @@ import { useSyncExternalStore } from "react"; -import { globalStore } from "../visualizers/bubble-sort"; +import { globalStore } from "../visualizers/dbscan"; export const useVisualizer = () => { return useSyncExternalStore(globalStore.subscribe, globalStore.getCurSnapshot);