Skip to content

Commit

Permalink
Merge branch 'develop' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
normanius committed Mar 31, 2021
2 parents 88884d2 + 4639045 commit 9f04fa2
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
19 changes: 14 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@
- 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
- Fix: Animated example for matplotlib>=3.3
52 changes: 36 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -62,19 +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 setup.py build_ext --inplace
python setup.py sdist bdist_wheel
python test/test_all.py
python examples/examples.py
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"
```

## 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"
```
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.0.0
2.1.0
1 change: 1 addition & 0 deletions deploy.sh → build_install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions deploy_test.sh → build_install_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
85 changes: 85 additions & 0 deletions examples/comparison.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""
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.
"""

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)
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)
12 changes: 7 additions & 5 deletions examples/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -135,5 +137,5 @@ def benchmark_with_details():
################################################################################
if __name__ == "__main__":
example_basic()
example_animated()
anim = example_animated()
plt.show()
6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
7 changes: 5 additions & 2 deletions src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
33 changes: 33 additions & 0 deletions src/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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"]
9 changes: 0 additions & 9 deletions src/_miniball_wrap.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 23 additions & 2 deletions tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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__":
Expand Down

0 comments on commit 9f04fa2

Please sign in to comment.