Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 43 additions & 14 deletions .github/workflows/build_wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,50 +26,78 @@ jobs:
cd build
cmake ..
make -j

- name: Test
run: |
cd build
ctest --no-tests=error --output-on-failure

- uses: actions/upload-artifact@v3
with:
name: executable
path: ./build/executable/dbscan

generate_matrix:
name: Generate Matrix
runs-on: ubuntu-latest

outputs:
OSES: ${{ steps.development.outputs.OSES || steps.production.outputs.OSES }}
CIBW_ARCHS_LINUX: ${{ steps.development.outputs.CIBW_ARCHS_LINUX || steps.production.outputs.CIBW_ARCHS_LINUX }}

steps:
- name: Development
id: development
if: "!startsWith(github.ref, 'refs/tags/v')"
run: |
echo 'OSES=["ubuntu-latest"]' >> $GITHUB_OUTPUT
echo 'CIBW_ARCHS_LINUX="auto"' >> $GITHUB_OUTPUT

- name: Production
id: production
if: startsWith(github.ref, 'refs/tags/v')
run: |
echo 'OSES=["ubuntu-latest", "macos-latest", "windows-latest"]' >> $GITHUB_OUTPUT
echo 'CIBW_ARCHS_LINUX="auto aarch64"' >> $GITHUB_OUTPUT

build_wheels:
needs: [generate_matrix]
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')
strategy:
matrix:
os: [ubuntu-latest]
os: ${{ fromJson(needs.generate_matrix.outputs.OSES) }}

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Set up QEMU
if: runner.os == 'Linux' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v'))
if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/v')
uses: docker/setup-qemu-action@v2
with:
platforms: all

- name: Build wheels (development)
if: github.ref != 'refs/heads/master' && !startsWith(github.ref, 'refs/tags/v')
uses: pypa/cibuildwheel@v2.11.2
env:
CIBW_ARCHS_MACOS: "arm64"

- name: Build wheels (production)
if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')
- name: Build wheels
uses: pypa/cibuildwheel@v2.11.2
env:
CIBW_ARCHS_MACOS: "x86_64 arm64"
CIBW_ARCHS_LINUX: "auto aarch64"
CIBW_ARCHS_LINUX: ${{ fromJson(needs.generate_matrix.outputs.CIBW_ARCHS_LINUX) }}
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_EXTRAS: "test36"
CIBW_TEST_COMMAND: "pytest {package}/test"

- uses: actions/upload-artifact@v3
with:
name: wheels
path: ./wheelhouse/*.whl

build_sdist:
name: Build source distribution
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v3
with:
Expand All @@ -80,10 +108,11 @@ jobs:

- uses: actions/upload-artifact@v3
with:
name: wheels
path: dist/*.tar.gz

upload_pypi:
needs: [build_and_test, build_wheels, build_sdist]
needs: [build_wheels, build_sdist]
runs-on: ubuntu-latest
# upload to PyPI on every tag starting with 'v'
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
Expand All @@ -94,7 +123,7 @@ jobs:
with:
# unpacks default artifact into dist/
# if `name: artifact` is omitted, the action will create extra parent dir
name: artifact
name: wheels
path: dist

- uses: pypa/gh-action-pypi-publish@v1.5.0
Expand Down
25 changes: 25 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ def initialize_options(self):

version = setuptools_scm.get_version()

with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()

setuptools.setup(
name="dbscan",
version=version,
Expand All @@ -60,4 +63,26 @@ def initialize_options(self):
'example': ['scikit-learn', 'matplotlib'],
},
zip_safe=False,

# To be removed when setuptools is good enough to support pyproject.toml
# completely.
author="Yiqiu Wang",
author_email="yiqiu_wang@icloud.com",
description="Theoretically efficient and practical parallel DBSCAN",
long_description=long_description,
long_description_content_type="text/markdown",
keywords='cluster clustering density dbscan',
url="https://github.com/wangyiqiu/dbscan-python",
license='MIT',
classifiers=[
'Development Status :: 2 - Pre-Alpha',
'Intended Audience :: Science/Research',
'Intended Audience :: Developers',
"License :: OSI Approved :: MIT License",
'Programming Language :: C++',
'Programming Language :: Python :: 3.8',
'Topic :: Software Development',
'Topic :: Scientific/Engineering',
"Operating System :: POSIX :: Linux",
],
)
47 changes: 47 additions & 0 deletions test/test_python_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
This file is almost identical to "example.py", except that it compares it
to the correct answer to create a test case.
"""

import numpy as np

from sklearn.datasets import make_blobs
from sklearn.preprocessing import StandardScaler

from sklearn.cluster import DBSCAN as goldDBSCAN
from dbscan import sklDBSCAN as ourDBSCAN

import unittest

class ExampleTest(unittest.TestCase):

def verify_output(self, X):
# ######################################################################
# Compute DBSCAN

labels = goldDBSCAN(eps=0.3, min_samples=10).fit(X).labels_
true_labels = ourDBSCAN(eps=0.3, min_samples=10).fit(X).labels_

# ######################################################################
# Test case

mapping = {-1: -1}

for i, (label, true_label) in enumerate(zip(labels, true_labels)):
if true_label in mapping:
self.assertTrue(
mapping[true_label] == label,
"Contradiction at slot {}. mapping={}. true={}. ours={}." \
.format(i, mapping, true_label, label)
)
else:
mapping[true_label] = label

def test_example(self):
# ######################################################################
# Generate sample data
centers = [[1, 1], [-1, -1], [1, -1]]
X, labels_true = make_blobs(n_samples=750, centers=centers,
cluster_std=0.4, random_state=0)
X = StandardScaler().fit_transform(X)
self.verify_output(X)