From c306ce2d75d51d4ed6f0df2498065bcf2dd04dc6 Mon Sep 17 00:00:00 2001 From: Norman Date: Mon, 7 Dec 2020 21:17:27 +0100 Subject: [PATCH 01/12] Added details in deploy scripts. --- deploy.sh | 1 + deploy_test.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/deploy.sh b/deploy.sh index 21aa7a0..f7a5195 100755 --- a/deploy.sh +++ b/deploy.sh @@ -7,6 +7,7 @@ rm -f src/_miniball_wrap.cpp rm -rf build rm -rf dist pip uninstall -y cyminiball +pip cache remove cyminiball # Build export CYMINIBALL_TRACE="0" diff --git a/deploy_test.sh b/deploy_test.sh index 22b1a67..a3d5bf4 100755 --- a/deploy_test.sh +++ b/deploy_test.sh @@ -10,6 +10,7 @@ rm -f src/_miniball_wrap.cpp rm -rf build rm -rf dist pip uninstall -y cyminiball +pip cache remove cyminiball # Build export CYMINIBALL_TRACE="1" From 3301aeb1caff23818029c4dd8d9fc1e56d0d9ed0 Mon Sep 17 00:00:00 2001 From: Norman Date: Mon, 7 Dec 2020 21:22:27 +0100 Subject: [PATCH 02/12] Rename build scripts --- .travis.yml | 2 +- README.md | 5 ++--- deploy.sh => build_install.sh | 0 deploy_test.sh => build_install_test.sh | 0 4 files changed, 3 insertions(+), 4 deletions(-) rename deploy.sh => build_install.sh (100%) rename deploy_test.sh => build_install_test.sh (100%) diff --git a/.travis.yml b/.travis.yml index 75188ca..2b211c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ install: - pip install Cython - pip install "pytest>=6.0" "pytest-cov" script: - - ./deploy_test.sh + - ./build_install_test.sh - pytest --version - pytest -c pyproject.toml after_success: diff --git a/README.md b/README.md index fb32329..b956b37 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,7 @@ To build the package requires ```bash git clone https://github.com/hirsch-lab/cyminiball.git cd cyminiball -python setup.py build_ext --inplace -python setup.py sdist bdist_wheel -python test/test_all.py +./build_install.sh +python tests/test_all.py python examples/examples.py ``` diff --git a/deploy.sh b/build_install.sh similarity index 100% rename from deploy.sh rename to build_install.sh diff --git a/deploy_test.sh b/build_install_test.sh similarity index 100% rename from deploy_test.sh rename to build_install_test.sh From ea6e4da01eab17522f91c6047c9b647eadf886c6 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 31 Mar 2021 01:03:46 +0200 Subject: [PATCH 03/12] Fix deprecation warnings --- examples/examples.py | 2 +- tests/test_all.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/examples.py b/examples/examples.py index 28cd220..860255c 100644 --- a/examples/examples.py +++ b/examples/examples.py @@ -7,7 +7,7 @@ ################################################################################ def generate_data(n=50): d = 2 # Number of dimensions - dt = np.float # Different data types are supported + dt = np.float64 # Different data types are supported rs = np.random.RandomState(42) points = rs.randn(n, d) points = points.astype(dt) diff --git a/tests/test_all.py b/tests/test_all.py index 4d3a5cf..304282b 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -9,7 +9,7 @@ class TestTypes(unittest.TestCase): np.float64: np.float64, np.float128: np.float128, # np.float16: float, # not available - np.float: float + # np.float: float # deprecated } valid_itypes = {int: float, @@ -25,7 +25,7 @@ class TestTypes(unittest.TestCase): # np.bool8: float, # supported, but not tested. # np.bool: float # supported, but not tested. } - invalid_dtypes = [str, np.str, np.complex, np.complex64, + invalid_dtypes = [str, complex, np.complex64, np.complex128, np.complex256, np.float16] iterables = [list, tuple, set] detailed = False From 35902fa1fc1b90fbddfffcd3f8d4a7e524187c7e Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 31 Mar 2021 01:05:27 +0200 Subject: [PATCH 04/12] Introduce _compat.py, mimic interface also fro miniballcpp --- src/__init__.py | 7 +++++-- src/_compat.py | 34 ++++++++++++++++++++++++++++++++++ src/_miniball_wrap.pyx | 9 --------- tests/test_all.py | 21 +++++++++++++++++++++ 4 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 src/_compat.py diff --git a/src/__init__.py b/src/__init__.py index 53d8d3c..18324bb 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -11,5 +11,8 @@ MiniballValueError, compute, compute_no_checks, - compute_max_chord, - get_bounding_ball) + compute_max_chord) + +# To mimic the interface of other miniball projects +from ._compat import (get_bounding_ball, + Miniball) diff --git a/src/_compat.py b/src/_compat.py new file mode 100644 index 0000000..e666dc6 --- /dev/null +++ b/src/_compat.py @@ -0,0 +1,34 @@ +from ._wrap import compute + + +def get_bounding_ball(points): + """An alias for miniball.compute() with the purpose to make the + cyminiball package a drop-in replacement for another miniball project + available on PyPI: https://pypi.org/project/miniball/ + """ + return compute(points, details=False, tol=None) + + +class Miniball: + """Mimic the interface of yet another miniball PyPI project: + https://pypi.org/project/MiniballCpp/ + """ + def __init__(self, points): + _, _, info = compute(points, details=True) + self._info = info + + def center(self): + return self._info["center"] + + def squared_radius(self): + return self._info["radius"]**2 + + def relative_error(self): + return self._info["relative_error"] + + def is_valid(self): + return self._info["is_valid"] + + def get_time(self): + return self._info["elapsed"] + diff --git a/src/_miniball_wrap.pyx b/src/_miniball_wrap.pyx index cd4f26b..15e9b9c 100644 --- a/src/_miniball_wrap.pyx +++ b/src/_miniball_wrap.pyx @@ -245,15 +245,6 @@ def compute(points, details=False, tol=None): return compute_no_checks(points, details, tol) -################################################################################ -def get_bounding_ball(points): - """An alias for miniball.compute() with the purpose to make the - cyminiball package a drop-in replacement for another miniball project - available on PyPi: https://pypi.org/project/miniball/ - """ - return compute(points, details=False, tol=None) - - ################################################################################ def compute_max_chord(points, info=None, details=False, tol=None): """Compute the longest chord between the support points of the miniball. diff --git a/tests/test_all.py b/tests/test_all.py index 304282b..cf6ac42 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -300,6 +300,9 @@ def test_plain(self): self.assertLessEqual(info["d_max"], upper) def test_get_bounding_ball(self): + """ + See miniball._compat.get_bounding_ball() + """ for dt in self.dtypes: with self.subTest(dt=dt): data = self.rs.normal(0, 1, (100, 5)) @@ -309,6 +312,24 @@ def test_get_bounding_ball(self): np.testing.assert_array_equal(C_A, C_B) self.assertEqual(r2_A, r2_B) + def test_miniballcpp_interface(self): + """ + See miniball._compat.Miniball() + """ + for dt in self.dtypes: + with self.subTest(dt=dt): + data = self.rs.normal(0, 1, (100, 5)) + data = data.astype(dt) + C_A, r2_A, info = mb.compute(data, details=True) + M = mb.Miniball(data) + C_B = M.center() + r2_B = M.squared_radius() + np.testing.assert_array_equal(C_A, C_B) + self.assertEqual(r2_A, r2_B) + self.assertEqual(info["relative_error"], M.relative_error()) + self.assertEqual(info["is_valid"], M.is_valid()) + np.testing.assert_almost_equal(info["elapsed"], M.get_time(), 3) + ################################################################################ if __name__ == "__main__": From 01a384e1294033c1b0fc81b30dcee6d848a4b177 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 31 Mar 2021 01:13:58 +0200 Subject: [PATCH 05/12] Add examples/comparison.py --- examples/comparison.py | 84 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 examples/comparison.py diff --git a/examples/comparison.py b/examples/comparison.py new file mode 100644 index 0000000..51838c7 --- /dev/null +++ b/examples/comparison.py @@ -0,0 +1,84 @@ +import timeit +import numpy as np +import miniball # miniballcpp or miniball +import cyminiball +import matplotlib.pyplot as plt + +""" +Compare different implementations of miniball: + +1. cyminiball: https://pypi.org/project/cyminiball/ +2. miniballcpp: https://pypi.org/project/MiniballCpp/ +3. miniball: https://pypi.org/project/miniball/ + +Since miniball and miniballcpp have the same package name (miniball), they +cannot be used side by side. The former is very slow, hence it is excluded here. +""" + +################################################################################ +def create_data(n, d, dt): + points = np.random.randn(n, d) + points = points.astype(dt) + return points + + +################################################################################ +def run_timer(statement, context): + timer = timeit.Timer(statement, globals=context) + n_reps, delta = timer.autorange() + return delta/n_reps, n_reps + + +################################################################################ +def measure_performance(d=2, dt=np.float64, n_steps=10): + a_min = 3 + a_max = 7 + ns = np.logspace(a_min, a_max, n_steps).astype(int) + t1 = np.zeros(len(ns)) + t2 = np.zeros(len(ns)) + statement1 = "cyminiball.compute(points)" + statement2 = "miniball.Miniball(points)" + #statement2 = "miniball.get_bounding_ball(points)" + + print("Running...") + + for i,n in enumerate(ns): + points = create_data(n=n, d=d, dt=dt) + context = dict(miniball=miniball, cyminiball=cyminiball, points=points) + delta1, n_reps1 = run_timer(statement=statement1, context=context) + t1[i] = delta1 + delta2, n_reps2 = run_timer(statement=statement2, context=context) + t2[i] = delta2 + print(f"%2d/%2d (n: %{a_max+1}d, d: %d, n_reps: %d, %d)" + % (i+1, len(ns), n, d, n_reps1, n_reps2)) + + ratio = t2/t1 + print("Done!") + print() + print("ratio: %.2f ± %.2f" % (ratio.mean(), ratio.std())) + print("ratios: %s" % ", ".join(map(str, ratio.round(1)))) + return ns, t1, t2 + + +################################################################################ +def plot_results(ns, t1, t2, d, dt): + fig, ax = plt.subplots() + ax.plot(ns, t1*1000, "-o", label="cyminiball") + ax.plot(ns, t2*1000, "-x", label="miniball") + ax.set_xlabel("Number of points") + ax.set_ylabel("Time (ms)") + ax.set_title("Point cloud: d=%d, type=%s" % (d, dt.__name__)) + ax.legend() + ax.grid(True) + ax.set_yscale("log") + ax.set_xscale("log") + plt.show() + + +################################################################################ +if __name__ == "__main__": + d = 2 + dt = np.float64 + n_steps = 10 + ns, t1, t2 = measure_performance(d=d, n_steps=n_steps) + plot_results(ns=ns, t1=t1, t2=t2, d=d, dt=dt) From e4a0d170057e4124e0b33f376ee079e3d909b5bd Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 31 Mar 2021 01:14:25 +0200 Subject: [PATCH 06/12] Add requirements.txt --- requirements.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..68be9ba --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +# Install with pip: +# python -m pip install -r requirements.txt +numpy +Cython>=0.29 +pytest-cov # for test-coverage +miniballcpp # in examples/comparison.py From 2faee20ac137140284205b10bd5ee719eb5335ae Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 31 Mar 2021 01:21:11 +0200 Subject: [PATCH 07/12] Update docs --- README.md | 49 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b956b37..a244ce1 100644 --- a/README.md +++ b/README.md @@ -12,25 +12,30 @@ [![DeepSource](https://deepsource.io/gh/hirsch-lab/cyminiball.svg/?label=active+issues)](https://deepsource.io/gh/hirsch-lab/cyminiball/?ref=repository-badge) -Cyminiball is a utility to compute the smallest bounding ball of a point cloud in arbitrary dimensions. A Python/Cython binding of the popular [miniball](https://people.inf.ethz.ch/gaertner/subdir/software/miniball.html) utility by Bernd Gärtner. +A Python package to compute the smallest bounding ball of a point cloud in arbitrary dimensions. A Python/Cython binding of the popular [miniball](https://people.inf.ethz.ch/gaertner/subdir/software/miniball.html) utility by Bernd Gärtner. -The code is provided under the LGPLv3 license. +To my knowledge, this is currently the fastest implementation available in Python. For other implementations see: -For an implementation in pure Python, see [`miniball`](https://pypi.org/project/miniball/). `cyminiball` can be used as a drop-in replacement for `miniball`; it runs much faster because it is based on an effcient C++ implementation. +- [`miniballcpp`](https://pypi.org/project/MiniballCpp/) Python binding of the same C++ source ([miniball](https://people.inf.ethz.ch/gaertner/subdir/software/miniball.html)) +- [`miniball`](https://pypi.org/project/miniball/) Pure Python implementation (slow) -### Installation: +## Installation: - pip install cyminiball +The package is available via pip. -### Usage: +```bash +python -m pip install cyminiball +``` + +## Usage: ```python import cyminiball as miniball import numpy as np -d = 2 # Number of dimensions -n = 10000 # Number of points -dt = np.float # Data type +d = 2 # Number of dimensions +n = 10000 # Number of points +dt = np.float64 # Data type points = np.random.randn(n, d) points = points.astype(dt) @@ -62,18 +67,34 @@ C, r2, info = miniball.compute(points, details=True) See [examples/examples.py](https://github.com/hirsch-lab/cyminiball) for further usage examples -### Build +## Build package -To build the package requires +Building the package requires - Python 3.x - Cython - numpy +First, download the project and set up the environment. + ```bash -git clone https://github.com/hirsch-lab/cyminiball.git +git clone "https://github.com/hirsch-lab/cyminiball.git" cd cyminiball +python -m pip install -r "requirements.txt" +``` + +Then build and install the package. Run the tests/examples to verify the package. + +```bash ./build_install.sh -python tests/test_all.py -python examples/examples.py +python "tests/test_all.py" +python "examples/examples.py" ``` + +## Performance + +For a comparison with [`miniballcpp`](https://pypi.org/project/MiniballCpp/), run the below command. In my experiments, the Cython-optimized code ran 10-50 times faster, depending on the number of points and point dimensions. + +```bash +python "examples/comparison.py" +``` \ No newline at end of file From 303f26b795e593e65b5701635fa8edaab7832391 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 31 Mar 2021 01:35:14 +0200 Subject: [PATCH 08/12] Fix minor issues --- examples/comparison.py | 13 +++++++------ src/_compat.py | 1 - 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/comparison.py b/examples/comparison.py index 51838c7..b5e05f2 100644 --- a/examples/comparison.py +++ b/examples/comparison.py @@ -1,9 +1,3 @@ -import timeit -import numpy as np -import miniball # miniballcpp or miniball -import cyminiball -import matplotlib.pyplot as plt - """ Compare different implementations of miniball: @@ -15,6 +9,13 @@ cannot be used side by side. The former is very slow, hence it is excluded here. """ +import timeit +import numpy as np +import miniball # miniballcpp or miniball +import cyminiball +import matplotlib.pyplot as plt + + ################################################################################ def create_data(n, d, dt): points = np.random.randn(n, d) diff --git a/src/_compat.py b/src/_compat.py index e666dc6..4781a1a 100644 --- a/src/_compat.py +++ b/src/_compat.py @@ -31,4 +31,3 @@ def is_valid(self): def get_time(self): return self._info["elapsed"] - From c768779b4768e158f81d256fff866103e1dd146f Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 31 Mar 2021 01:35:45 +0200 Subject: [PATCH 09/12] Update CHANGELOG --- CHANGELOG.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 443b9b5..5fe947c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,19 @@ - First functional release ## v2.0.0 (07.12.2020) -- Enhancement: Renamed package cyminiball to comply with [PEP 423](https://www.python.org/dev/peps/pep-0423/) -- Enhancement: Refactored function signature of `compute_max_chord()` -- Enhancement: Added argument `tol` to control numerical validity check if (applies if `details=True`) -- Enhancement: Introduced basic setup for CI tools +- Enhancement: Rename package cyminiball to comply with [PEP 423](https://www.python.org/dev/peps/pep-0423/) +- Enhancement: Refactor function signature of `compute_max_chord()` +- Enhancement: Add argument `tol` to control numerical validity check if (applies if `details=True`) +- Enhancement: Introduce basic setup for CI tools - Enhancement: Enable coverage analysis for Cython code -- Enhancement: Improved coding style and test coverage +- Enhancement: Improve coding style and test coverage - Fix: Issue #1 Performance problem related to `compute(..., details=True)` - Fix: Issue #2 (suppress unnecessary runtime errors) + + +## v2.1.0 (31.03.2021) +- Enhancement: Update documenation +- Enhancement: Add requirements.txt +- Miscellaneous: Add reference to project `miniballcpp` +- Miscellaneous: Add examples/comparison.py +- Fix: Deprecation warnings \ No newline at end of file From d6519f2ad75de01a9f9e86420d942c9d57df0d80 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 31 Mar 2021 01:39:28 +0200 Subject: [PATCH 10/12] Bump version --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 227cea2..7ec1d6d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.0 +2.1.0 From 66e0566989a2724cd424fe883f9bc2ec77ee69da Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 31 Mar 2021 02:13:24 +0200 Subject: [PATCH 11/12] Fix animated example --- examples/examples.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/examples.py b/examples/examples.py index 860255c..7874e69 100644 --- a/examples/examples.py +++ b/examples/examples.py @@ -95,10 +95,12 @@ def update(x): [points[-1, 1], points[-1, 1]]]) return circle, center, line, point + # Info: Blitting seems not to work with OSX backend. + # (Check the backend that is set in .matplotlibrc) from matplotlib.animation import FuncAnimation - FuncAnimation(fig, update, frames=xrange, interval=30, - init_func=init, blit=True) - + anim = FuncAnimation(fig, update, frames=xrange, interval=30, + init_func=init, blit=True) + return anim ################################################################################ def benchmark_with_details(): @@ -135,5 +137,5 @@ def benchmark_with_details(): ################################################################################ if __name__ == "__main__": example_basic() - example_animated() + anim = example_animated() plt.show() From 4639045266a75b6e88b767d7b9b1683937b0be82 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 31 Mar 2021 02:15:39 +0200 Subject: [PATCH 12/12] Update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe947c..5d54e58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,4 +17,5 @@ - Enhancement: Add requirements.txt - Miscellaneous: Add reference to project `miniballcpp` - Miscellaneous: Add examples/comparison.py -- Fix: Deprecation warnings \ No newline at end of file +- Fix: Deprecation warnings +- Fix: Animated example for matplotlib>=3.3 \ No newline at end of file