Skip to content
This repository was archived by the owner on Oct 17, 2021. It is now read-only.

Commit e291561

Browse files
committed
Initial commit
0 parents  commit e291561

File tree

9 files changed

+326
-0
lines changed

9 files changed

+326
-0
lines changed

.github/workflows/ci.yml

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: CI
2+
3+
on: [push]
4+
5+
jobs:
6+
macos:
7+
runs-on: macOS-latest
8+
9+
steps:
10+
- name: Checkout
11+
uses: actions/checkout@v1
12+
- name: Build and Test
13+
run: swift test
14+
15+
linux:
16+
runs-on: ubuntu-latest
17+
18+
strategy:
19+
matrix:
20+
swift: ["5.1"]
21+
22+
container:
23+
image: swift:${{ matrix.swift }}
24+
25+
steps:
26+
- name: Checkout
27+
uses: actions/checkout@v1
28+
- name: Build and Test
29+
run: swift test --enable-test-discovery

.github/workflows/documentation.yml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Documentation
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
paths:
8+
- Sources
9+
10+
jobs:
11+
build:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v1
17+
- name: Generate Documentation
18+
uses: SwiftDocOrg/swift-doc@master
19+
with:
20+
inputs: "Sources"
21+
output: "Documentation"
22+
- name: Upload Documentation to Wiki
23+
uses: SwiftDocOrg/github-wiki-publish-action@master
24+
with:
25+
path: "Documentation"
26+
env:
27+
GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }}

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/
6+
.swiftpm

LICENSE.md

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright 2020 Read Evaluate Press, LLC
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a
4+
copy of this software and associated documentation files (the "Software"),
5+
to deal in the Software without restriction, including without limitation
6+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
7+
and/or sell copies of the Software, and to permit persons to whom the
8+
Software is furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in
11+
all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
14+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19+
DEALINGS IN THE SOFTWARE.

Package.swift

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// swift-tools-version:5.1
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "DBSCAN",
8+
products: [
9+
// Products define the executables and libraries produced by a package, and make them visible to other packages.
10+
.library(
11+
name: "DBSCAN",
12+
targets: ["DBSCAN"]),
13+
],
14+
dependencies: [
15+
// Dependencies declare other packages that this package depends on.
16+
// .package(url: /* package url */, from: "1.0.0"),
17+
],
18+
targets: [
19+
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
20+
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
21+
.target(
22+
name: "DBSCAN",
23+
dependencies: []),
24+
.testTarget(
25+
name: "DBSCANTests",
26+
dependencies: ["DBSCAN"]),
27+
]
28+
)

README.md

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# DBSCAN
2+
3+
**D**ensity-**b**ased **s**patial **c**lustering of **a**pplications with **n**oise
4+
([DBSCAN](https://en.wikipedia.org/wiki/DBSCAN)).
5+
6+
## Usage
7+
8+
```swift
9+
import DBSCAN
10+
import simd
11+
12+
let input: [SIMD3<Double>] = [[ 0, 10, 20 ],
13+
[ 0, 11, 21 ],
14+
[ 0, 12, 20 ],
15+
[ 20, 33, 59 ],
16+
[ 21, 32, 56 ],
17+
[ 59, 77, 101 ],
18+
[ 58, 79, 100 ],
19+
[ 58, 76, 102 ],
20+
[ 300, 70, 20 ],
21+
[ 500, 300, 202],
22+
[ 500, 302, 204 ]]
23+
24+
let dbscan = DBSCAN(input)
25+
26+
#if swift(>=5.2)
27+
let (clusters, outliers) = dbscan(epsilon: 10,
28+
minimumNumberOfPoints: 1,
29+
distanceFunction: simd.distance)
30+
#else // Swift <5.2 requires explicit `callAsFunction` method name
31+
let (clusters, outliers) = dbscan.callAsFunction(epsilon: 10,
32+
minimumNumberOfPoints: 1,
33+
distanceFunction: simd.distance)
34+
#endif
35+
36+
print(clusters)
37+
// [ [0, 10, 20], [0, 11, 21], [0, 12, 20] ]
38+
// [ [20, 33, 59], [21, 32, 56] ],
39+
// [ [58, 79, 100], [58, 76, 102], [59, 77, 101] ],
40+
// [ [500, 300, 202], [500, 302, 204] ],
41+
42+
print(outliers)
43+
// [ [ 300, 70, 20 ] ]
44+
```
45+
46+
## Requirements
47+
48+
- Swift 5.1+
49+
50+
## Installation
51+
52+
### Swift Package Manager
53+
54+
Add the SwiftMarkup package to your target dependencies in `Package.swift`:
55+
56+
```swift
57+
import PackageDescription
58+
59+
let package = Package(
60+
name: "YourProject",
61+
dependencies: [
62+
.package(
63+
url: "https://github.com/NSHipster/DBSCAN",
64+
from: "0.0.1"
65+
),
66+
]
67+
)
68+
```
69+
70+
Then run the `swift build` command to build your project.
71+
72+
## License
73+
74+
MIT
75+
76+
## Contact
77+
78+
Mattt ([@mattt](https://twitter.com/mattt))
79+
80+
[SE-0253]: https://github.com/apple/swift-evolution/blob/master/proposals/0253-callable.md "Callable values of user-defined nominal types"

Sources/DBSCAN/DBSCAN.swift

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
A density-based, non-parametric clustering algorithm
3+
([DBSCAN](https://en.wikipedia.org/wiki/DBSCAN)).
4+
5+
Given a set of points in some space,
6+
this algorithm groups points with many nearby neighbors
7+
and marks points in low-density regions as outliers.
8+
9+
- Authors: Ester, Martin; Kriegel, Hans-Peter; Sander, Jörg; Xu, Xiaowei (1996)
10+
"A density-based algorithm for discovering clusters
11+
in large spatial databases with noise."
12+
_Proceedings of the Second International Conference on
13+
Knowledge Discovery and Data Mining (KDD-96)_.
14+
*/
15+
public struct DBSCAN<Value: Equatable> {
16+
private class Point: Equatable {
17+
typealias Label = Int
18+
19+
let value: Value
20+
var label: Label?
21+
22+
init(_ value: Value) {
23+
self.value = value
24+
}
25+
26+
static func == (lhs: Point, rhs: Point) -> Bool {
27+
return lhs.value == rhs.value
28+
}
29+
}
30+
31+
/// The values to be clustered.
32+
public var values: [Value]
33+
34+
/// Creates a new clustering algorithm with the specified values.
35+
/// - Parameter values: The values to be clustered.
36+
public init(_ values: [Value]) {
37+
self.values = values
38+
}
39+
40+
41+
/**
42+
Clusters values according to the specified parameters.
43+
44+
- Parameters:
45+
- epsilon: The maximum distance from a specified value
46+
for which other values are considered to be neighbors.
47+
- minimumNumberOfPoints: The minimum number of points
48+
required to form a dense region.
49+
- distanceFunction: A function that computes
50+
the distance between two values.
51+
- Throws: Rethrows any errors produced by `distanceFunction`.
52+
- Returns: A tuple containing an array of clustered values
53+
and an array of outlier values.
54+
*/
55+
public func callAsFunction(epsilon: Double, minimumNumberOfPoints: Int, distanceFunction: (Value, Value) throws -> Double) rethrows -> (clusters: [[Value]], outliers: [Value]) {
56+
precondition(minimumNumberOfPoints >= 0)
57+
58+
let points = values.map { Point($0) }
59+
60+
var currentLabel = 0
61+
for point in points {
62+
guard point.label == nil else { continue }
63+
64+
var neighbors = try points.filter { try distanceFunction(point.value, $0.value) < epsilon }
65+
if neighbors.count >= minimumNumberOfPoints {
66+
defer { currentLabel += 1 }
67+
point.label = currentLabel
68+
69+
while !neighbors.isEmpty {
70+
let neighbor = neighbors.removeFirst()
71+
guard neighbor.label == nil else { continue }
72+
73+
neighbor.label = currentLabel
74+
75+
let n1 = try points.filter { try distanceFunction(neighbor.value, $0.value) < epsilon }
76+
if n1.count >= minimumNumberOfPoints {
77+
neighbors.append(contentsOf: n1)
78+
}
79+
}
80+
}
81+
}
82+
83+
var clusters: [[Value]] = []
84+
var outliers: [Value] = []
85+
86+
for points in Dictionary(grouping: points, by: { $0.label }).values {
87+
let values = points.map { $0.value }
88+
if values.count == 1 {
89+
outliers.append(contentsOf: values)
90+
} else {
91+
clusters.append(values)
92+
}
93+
}
94+
95+
return (clusters, outliers)
96+
}
97+
}

Tests/DBSCANTests/DBSCANTests.swift

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import XCTest
2+
@testable import DBSCAN
3+
4+
import simd
5+
6+
final class DBSCANTests: XCTestCase {
7+
func testExample() {
8+
let input: [SIMD3<Double>] = [[ 0, 10, 20 ],
9+
[ 0, 11, 21 ],
10+
[ 0, 12, 20 ],
11+
[ 20, 33, 59 ],
12+
[ 21, 32, 56 ],
13+
[ 59, 77, 101 ],
14+
[ 58, 79, 100 ],
15+
[ 58, 76, 102 ],
16+
[ 300, 70, 20 ],
17+
[ 500, 300, 202],
18+
[ 500, 302, 204 ]]
19+
20+
let dbscan = DBSCAN(input)
21+
22+
#if swift(>=5.2)
23+
let (clusters, outliers) = dbscan(epsilon: 10, minimumNumberOfPoints: 1, distanceFunction: simd.distance)
24+
#else
25+
let (clusters, outliers) = dbscan.callAsFunction(epsilon: 10, minimumNumberOfPoints: 1, distanceFunction: simd.distance)
26+
#endif
27+
28+
XCTAssertEqual(clusters.count, 4)
29+
XCTAssertEqual(Set(clusters.map { Set($0) }), [
30+
[ [20.0, 33.0, 59.0], [21.0, 32.0, 56.0] ],
31+
[ [58.0, 79.0, 100.0], [58.0, 76.0, 102.0], [59.0, 77.0, 101.0] ],
32+
[ [500.0, 300.0, 202.0], [500.0, 302.0, 204.0] ],
33+
[ [0.0, 10.0, 20.0], [0.0, 11.0, 21.0], [0.0, 12.0, 20.0] ]
34+
])
35+
36+
XCTAssertEqual(outliers.count, 1)
37+
XCTAssertEqual(outliers[0], [300.0, 70.0, 20.0])
38+
}
39+
}

Tests/LinuxMain.swift

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fatalError("Run with `swift test --enable-test-discovery`")

0 commit comments

Comments
 (0)