Repository: severinstrobl/overlap (language: python)

Issue:
Provide Python bindings for `overlap`
Hi there! Your overlap library has many more applications than you might imagine :) I'd like to use it in the data science world which is Python dominated. Is it possible to provide a compiled version of this as a Python package? There are things like `pybind` that will automatically generate Python bindings from your header file.

I have 0 experience on the C++ side but would be interested to help you out wherever I can on the Python side, testing, packaging etc!


Discussion / hints:
Interestingly enough, that is actually something I thought about recently, but didn't really have a use-case for so far. Generating Python wrappers using `pybind11` should be pretty straight forward based on some past experiments. Now that I know there are potential users for such bindings, I'll look into this asap. I'll keep you posted and would certainly appreciate help in testing, etc.!

Fix patch:
diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml
new file mode 100644
index 0000000..1b7d7c7
--- /dev/null
+++ b/.github/workflows/python.yaml
@@ -0,0 +1,38 @@
+name: CI Python Linux
+
+on:
+  - push
+  - pull_request
+
+jobs:
+  build:
+    name: "🐍 Ubuntu 20.04 - Python ${{ matrix.python-version }}"
+    runs-on: ubuntu-20.04
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: [3.5, 3.6, 3.7, 3.8, 3.9]
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          submodules: true
+
+      - name: Setup Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Dependencies
+        run: |
+          sudo apt install libeigen3-dev
+          python -m pip install pytest
+
+      - name: Install
+        run: |
+          python -m pip install .
+
+      - name: Test
+        run: |
+          pytest
diff --git a/.github/workflows/wheels.yaml b/.github/workflows/wheels.yaml
new file mode 100644
index 0000000..e96b1f5
--- /dev/null
+++ b/.github/workflows/wheels.yaml
@@ -0,0 +1,75 @@
+name: Python Wheels
+
+on:
+  pull_request:
+  push:
+    branches:
+      - python
+
+jobs:
+  build_sdist:
+    name: Build 🐍 sdist
+    runs-on: ubuntu-20.04
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          submodules: true
+
+      - name: Setup Python
+        uses: actions/setup-python@v2
+
+      - name: Dependencies
+        run: python -m pip install scikit-build twine
+
+      - name: Build sdist
+        run: python setup.py sdist
+
+      - name: Check metadata
+        run: twine check dist/*
+
+      - name: Upload Artifacts
+        uses: actions/upload-artifact@v2
+        with:
+          path: dist/*.tar.gz
+          retention-days: 2
+
+  build_wheels:
+    name: Build 🐍 wheels on ${{ matrix.os }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-20.04, macos-latest, windows-2016]
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          submodules: true
+
+      - name: Setup Python
+        uses: actions/setup-python@v2
+
+      - name: Dependencies
+        run: |
+          python -m pip install cibuildwheel==1.7.4
+
+      - name: Build wheels
+        env:
+          CIBW_SKIP: "cp27-* pp27-* cp35-*"
+          CIBW_BEFORE_BUILD_MACOS: >
+            brew install openblas &&
+            export OPENBLAS="$(brew --prefix openblas)" &&
+            python -m pip install numpy
+          CIBW_TEST_REQUIRES: pytest
+          CIBW_TEST_COMMAND: "python -m pytest {project}/test"
+        run: |
+          python -m cibuildwheel --output-dir wheelhouse
+
+      - name: Upload Artifacts
+        uses: actions/upload-artifact@v2
+        with:
+          path: ./wheelhouse/*.whl
+          retention-days: 2
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index 8d4f8c9..6d5015c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,9 @@
 [submodule "third_party/googletest"]
 	path = third_party/googletest
 	url = https://github.com/google/googletest
+[submodule "third_party/pybind11"]
+	path = third_party/pybind11
+	url = https://github.com/pybind/pybind11
+[submodule "third_party/eigen"]
+	path = third_party/eigen
+	url = https://gitlab.com/libeigen/eigen
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6ab89f1..55ec729 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,5 +1,5 @@
 cmake_minimum_required(VERSION 3.10)
-project(overlap)
+project(overlap LANGUAGES CXX)
 
 option(OVERLAP_TESTS "Build the tests" on)
 option(OVERLAP_COVERAGE "Build with coverage support" off)
@@ -10,23 +10,19 @@ list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
 include_directories(${PROJECT_SOURCE_DIR})
 
 # Add compiler flags for C++11.
-if(${CMAKE_VERSION} VERSION_LESS "3.1")
-    if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
-        "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
-        set(CMAKE_CXX_FLAGS "--std=c++11 ${CMAKE_CXX_FLAGS}")
-    else()
-        message(FATAL_ERROR
-            "Unsupported compiler detected: ${CMAKE_CXX_COMPILER_ID}")
-    endif()
-else()
-    set(CMAKE_CXX_STANDARD 11)
-    set(CMAKE_CXX_STANDARD_REQUIRED ON)
-    set(CMAKE_CXX_EXTENSIONS OFF)
-endif()
+set(CMAKE_CXX_STANDARD 11)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
 
 # Set up the Eigen3 library.
-find_package(Eigen3 3.0.0 REQUIRED)
-include_directories(${EIGEN3_INCLUDE_DIR})
+find_package(Eigen3 3.2.0 NO_MODULE)
+if(NOT TARGET Eigen3::Eigen)
+    message(STATUS "Failed to find Eigen3 >= 3.2.0, using internal version")
+
+    add_library(Eigen3::Eigen IMPORTED INTERFACE)
+    set_property(TARGET Eigen3::Eigen PROPERTY INTERFACE_INCLUDE_DIRECTORIES
+                 "${PROJECT_SOURCE_DIR}/third_party/eigen")
+endif()
 
 # Add dummy interface for coverage configuration.
 add_library(coverage INTERFACE)
@@ -47,3 +43,9 @@ if(OVERLAP_TESTS)
     enable_testing()
     add_subdirectory(test)
 endif()
+
+# Build the Python bindings using pybind11.
+option(OVERLAP_PYTHON "Build the Python bindings" ON)
+if(OVERLAP_PYTHON)
+    add_subdirectory(python)
+endif()
diff --git a/README.md b/README.md
index e594a6d..b9cbd4d 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,9 @@ compilers:
 Additionally, the Intel C++ compiler starting with version 15.0 should work,
 albeit this configuration is not part of the CI process.
 
+
+## C++
+
 The library is implemented as a pure header-only library written in plain
 C++11. To use it in your code, simply include the header file `overlap.hpp` and
 make sure the **Eigen3** headers can be found by your compiler or build system.
@@ -118,6 +121,38 @@ std::cout << "total surface area of tetrahedron intersecting sphere: " <<
     result.back() << std::endl;
 ```
 
+## Python
+
+The Python version of the `overlap` library is available via the [Python
+Package Index (PyPI)](https://pypi.org/project/overlap/), so for most
+environments installation should be possible simply via `pip install overlap`.
+
+In case no pre-built package or *wheel* is available for your system, compilation of the
+wrapper code is required which in turn requires the requirements listed above
+for the C++ version to be fulfilled.
+
+The interface of Python version closely resembles the interface of the C++ version:
+
+```python
+import numpy as np
+import overlap
+
+vertices = np.array((
+    (-1, -np.sqrt(1./3.), -np.sqrt(1./6.)),
+    (1, -np.sqrt(1./3.), -np.sqrt(1./6.)),
+    (0, np.sqrt(4./3.), -np.sqrt(1./6.)),
+    (0, 0, np.sqrt(3./2.))
+))
+
+tet = overlap.Tetrahedron(vertices)
+sphere = overlap.Sphere((0, 0, 0.5), 1)
+
+result = overlap.overlap(sphere, tet)
+```
+
+Calculation of the overlap area instead of the overlap volume is possible via
+the function `overlap_area()` of the package.
+
 # License
 
 The `overlap` library is distributed under the GNU General Public
@@ -129,7 +164,7 @@ license terms. For details please consult the corresponding license terms
 included with each package in the respective subdirectory.
 
 | Package     | License |
-|-------------|----------|---------|
+|-------------|----------|
 | [Eigen](http://eigen.tuxfamily.org) | MPL2 |
 | [Google Test](https://github.com/google/googletest) | 3-clause BSD |
 | [pybind11](https://github.com/pybind/pybind11) | 3-clause BSD |
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..4cbf881
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,14 @@
+[build-system]
+requires = [
+    "scikit-build>=0.8.0",
+    "setuptools>=42",
+    "wheel",
+]
+
+build-backend = "setuptools.build_meta"
+
+[tool.pytest.ini_options]
+minversion = "6.0"
+testpaths = [
+    "test",
+]
\ No newline at end of file
diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt
new file mode 100644
index 0000000..b996c13
--- /dev/null
+++ b/python/CMakeLists.txt
@@ -0,0 +1,13 @@
+# Build pybind11 shipped as a third-party dependency.
+add_subdirectory("${PROJECT_SOURCE_DIR}/third_party/pybind11"
+                 "third_party/pybind11"
+                 EXCLUDE_FROM_ALL)
+
+pybind11_add_module(_overlap MODULE overlap.cpp)
+
+target_link_libraries(_overlap PRIVATE Eigen3::Eigen)
+
+# Forward version info from setup.py.
+#target_compile_definitions(overlap PRIVATE VERSION_INFO=${EXAMPLE_VERSION_INFO})
+
+install(TARGETS _overlap DESTINATION .)
\ No newline at end of file
diff --git a/python/__init__.py b/python/__init__.py
new file mode 100644
index 0000000..53871bc
--- /dev/null
+++ b/python/__init__.py
@@ -0,0 +1,1 @@
+from ._overlap import *
diff --git a/python/overlap.cpp b/python/overlap.cpp
new file mode 100644
index 0000000..84339e5
--- /dev/null
+++ b/python/overlap.cpp
@@ -0,0 +1,125 @@
+/*!
+ * Exact calculation of the overlap volume of spheres and mesh elements.
+ * http://dx.doi.org/10.1016/j.jcp.2016.02.003
+ *
+ * Copyright (C) 2021 Severin Strobl <git@severin-strobl.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <cctype>
+
+#include "pybind11/pybind11.h"
+#include "pybind11/eigen.h"
+#include "pybind11/numpy.h"
+#include "pybind11/stl.h"
+
+#include "overlap.hpp"
+
+namespace py = pybind11;
+
+template<typename... Args>
+using overload_cast_ = py::detail::overload_cast_impl<Args...>;
+
+using namespace py::literals;
+
+template<typename Element>
+void createBindings(py::module& m) {
+	static_assert(std::is_same<Element, Tetrahedron>::value ||
+		std::is_same<Element, Wedge>::value ||
+		std::is_same<Element, Hexahedron>::value,
+		"Invalid element type detected.");
+
+  static const std::string name = std::is_same<Element, Tetrahedron>::value ?
+    "Tetrahedron" : std::is_same<Element, Wedge>::value ?
+    "Wedge" : "Hexahedron";
+
+  static const std::string nameLower =
+    static_cast<char>(std::tolower(static_cast<char>(name[0]))) +
+    name.substr(1);
+
+  static constexpr std::size_t nrVertices = element_trait<Element>::nrVertices;
+
+  py::class_<Element>(m, name.c_str())
+    .def(py::init<std::array<vector_t, nrVertices>>())
+    .def(py::init([](py::array_t<double> vertices) {
+      auto proxy = vertices.unchecked<2>();
+      if(proxy.shape(0) != nrVertices || proxy.shape(1) != 3)
+        throw std::invalid_argument{"invalid shape for vertex list, must be (" +
+                                    std::to_string(nrVertices) + ", 3)"};
+
+      std::array<vector_t, nrVertices> tmp{};
+      for(py::ssize_t v = 0; v < proxy.shape(0); ++v)
+        tmp[v] = vector_t{proxy(v, 0), proxy(v, 1), proxy(v, 2)};
+
+      return Element(tmp);
+    }))
+    .def_readonly("vertices", &Element::vertices,
+      "Return the vertices of the element.")
+    .def_readonly("center", &Element::center,
+      "Return the center point of the element.")
+    .def_readonly("volume", &Element::volume,
+      "Return the volume of the element.")
+    .def_property_readonly("surface_area", [](const Element& elem) {
+        return elem.surfaceArea();
+      },
+      "Return the surface area of the element.");
+
+  m.def("overlap", overload_cast_<const Sphere&, const Element&>()
+    (&overlap<Element>), "sphere"_a, py::arg("nameLower.c_str()"),
+    ("Calculate the overlap volume of a sphere and a " + nameLower +
+      ".").c_str());
+
+  m.def("overlap_area", overload_cast_<const Sphere&, const Element&>()
+    (&overlapArea<Element>), "sphere"_a, py::arg("nameLower.c_str()"),
+    ("Calculate the overlap area of a sphere and a " + nameLower +
+      ".").c_str());
+}
+
+PYBIND11_MODULE(_overlap, m) {
+  m.doc() = R"pbdoc(
+        Pybind11 example plugin
+        -----------------------
+
+        This originates fom CPP
+
+        .. currentmodule:: overlap
+
+        .. autosummary::
+            :toctree: _generate
+    )pbdoc";
+
+#ifdef VERSION_INFO
+  m.attr("__version__") = VERSION_INFO;
+#else
+  m.attr("__version__") = "dev";
+#endif
+
+  py::class_<Sphere>(m, "Sphere")
+    .def(py::init<vector_t, scalar_t>())
+    .def_readonly("center", &Sphere::center,
+      "Return the center point of the sphere.")
+    .def_readonly("radius", &Sphere::radius,
+      "Return the radius of the sphere.")
+    .def_readonly("volume", &Sphere::volume,
+      "Return the volume of the sphere.")
+    .def_property_readonly("surface_area", [](const Sphere& s) {
+        return s.surfaceArea();
+      },
+      "Return the surface area of the sphere.");
+
+  createBindings<Tetrahedron>(m);
+  createBindings<Wedge>(m);
+  createBindings<Hexahedron>(m);
+}
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..5be287b
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,39 @@
+from skbuild import setup, exceptions, cmaker
+from packaging.version import LegacyVersion
+
+# Add CMake as a build requirement if no or only an outdated version is
+# available.
+setup_requires = []
+try:
+    if LegacyVersion(cmaker.get_cmake_version()) < LegacyVersion('3.10'):
+        setup_requires.append('cmake')
+except exceptions.SKBuildError:
+    setup_requires.append('cmake')
+
+with open('README.md', 'r', encoding='utf-8') as fh:
+    long_description = fh.read()
+
+setup(
+    name='overlap',
+    version='0.0.5',
+    author='Severin Strobl',
+    author_email='git@severin-strobl.de',
+    description='Exact calculation of the overlap volume and area of spheres '
+                'and mesh elements',
+    url='https://github.com/severinstrobl/overlap',
+    license='GPLv3',
+    long_description=long_description,
+    long_description_content_type='text/markdown',
+    packages=['overlap'],
+    package_dir={'overlap': 'python'},
+    setup_requires=setup_requires,
+    install_requires=[
+        'numpy'
+    ],
+    cmake_install_dir='python',
+    classifiers=[
+        'Programming Language :: Python :: 3',
+        'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
+        'Topic :: Scientific/Engineering'
+    ]
+)
diff --git a/third_party/eigen b/third_party/eigen
new file mode 160000
index 0000000..0fd6b4f
--- /dev/null
+++ b/third_party/eigen
@@ -0,0 +1,1 @@
+Subproject commit 0fd6b4f71dd85b2009ee4d1aeb296e2c11fc9d68
diff --git a/third_party/pybind11 b/third_party/pybind11
new file mode 160000
index 0000000..f1abf5d
--- /dev/null
+++ b/third_party/pybind11
@@ -0,0 +1,1 @@
+Subproject commit f1abf5d9159b805674197f6bc443592e631c9130


Test patch:
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index a465ccc..0ae9fff 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -1,5 +1,9 @@
-# Build GoogleTest shipped as a third-party dependency
-add_subdirectory("${PROJECT_SOURCE_DIR}/third_party/googletest" "third_party/googletest")
+# Build GoogleTest shipped as a third-party dependency.
+# Avoid linker issues on Windows when mixing shared and static libraries.
+set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
+add_subdirectory("${PROJECT_SOURCE_DIR}/third_party/googletest"
+                 "third_party/googletest"
+                 EXCLUDE_FROM_ALL)
 
 mark_as_advanced(
     BUILD_GMOCK BUILD_GTEST BUILD_SHARED_LIBS
@@ -47,4 +51,5 @@ set(UNIT_TESTS
 # Register the individual unit tests.
 foreach(UNIT_TEST ${UNIT_TESTS})
     overlap_add_test(${UNIT_TEST} "${UNIT_TEST}.cpp")
+    target_link_libraries(${UNIT_TEST} PUBLIC Eigen3::Eigen)
 endforeach()
diff --git a/test/detail_regularizedWedge.cpp b/test/detail_regularizedWedge.cpp
index 74283ee..a5b741c 100644
--- a/test/detail_regularizedWedge.cpp
+++ b/test/detail_regularizedWedge.cpp
@@ -18,6 +18,8 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
+#define _USE_MATH_DEFINES
+
 #include "gtest/gtest.h"
 
 #include "overlap.hpp"
diff --git a/test/detail_regularizedWedgeArea.cpp b/test/detail_regularizedWedgeArea.cpp
index 61d7754..270ab65 100644
--- a/test/detail_regularizedWedgeArea.cpp
+++ b/test/detail_regularizedWedgeArea.cpp
@@ -18,6 +18,8 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
+#define _USE_MATH_DEFINES
+
 #include "gtest/gtest.h"
 
 #include "overlap.hpp"
diff --git a/test/test_overlap.py b/test/test_overlap.py
new file mode 100644
index 0000000..cc7d365
--- /dev/null
+++ b/test/test_overlap.py
@@ -0,0 +1,126 @@
+# Exact calculation of the overlap volume of spheres and mesh elements.
+# http://dx.doi.org/10.1016/j.jcp.2016.02.003
+
+# Copyright (C) 2021 Severin Strobl <git@severin-strobl.de>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import numpy as np
+import overlap
+
+
+def test_sphere():
+    s = overlap.Sphere((1, 1, 1), 1)
+    np.testing.assert_almost_equal(s.center, [1, 1, 1])
+    np.testing.assert_almost_equal(s.radius, 1.0)
+    np.testing.assert_almost_equal(s.volume, 4./3. * np.pi)
+
+
+class TestElements:
+    def test_tetrahedron(self):
+        vertices = np.array((
+            (-1, -np.sqrt(1./3.), -np.sqrt(1./6.)),
+            (1, -np.sqrt(1./3.), -np.sqrt(1./6.)),
+            (0, np.sqrt(4./3.), -np.sqrt(1./6.)),
+            (0, 0, np.sqrt(3./2.))
+        ))
+        tet = overlap.Tetrahedron(vertices)
+
+        np.testing.assert_almost_equal(tet.volume, 8. / (6 * np.sqrt(2)))
+        np.testing.assert_almost_equal(tet.center, [0, 0, 0])
+        np.testing.assert_almost_equal(tet.surface_area, 4 * np.sqrt(3))
+
+    def test_wedge(self):
+        vertices = [
+            (-1, -1, -1), (1, -1, -1), (1,  1, -1),
+            (-1, -1,  1), (1, -1,  1), (1,  1,  1),
+        ]
+
+        wedge = overlap.Wedge(vertices)
+
+        np.testing.assert_almost_equal(wedge.volume, 4.0)
+        np.testing.assert_almost_equal(wedge.center, [1./3., -1./3., 0])
+        np.testing.assert_almost_equal(wedge.surface_area,
+                                       12.0 + 2 * np.sqrt(8))
+
+    def test_hexahedron(self):
+        vertices = [
+            (-1, -1, -1), (1, -1, -1), (1,  1, -1), (-1,  1, -1),
+            (-1, -1,  1), (1, -1,  1), (1,  1,  1), (-1,  1,  1)
+        ]
+
+        hexa = overlap.Hexahedron(vertices)
+
+        np.testing.assert_almost_equal(hexa.volume, 8.0)
+        np.testing.assert_almost_equal(hexa.center, [0, 0, 0])
+        np.testing.assert_almost_equal(hexa.surface_area, 24.0)
+
+
+class TestOverlap:
+    def test_wedge(self):
+        vertices = [
+            (-1, -1, -1), (1, -1, -1), (1,  1, -1),
+            (-1, -1,  1), (1, -1,  1), (1,  1,  1),
+        ]
+
+        wedge = overlap.Wedge(vertices)
+        s = overlap.Sphere((1, 1, 1), 1)
+
+        np.testing.assert_almost_equal(overlap.overlap(s, wedge), np.pi / 12)
+
+    def test_hexahedron(self):
+        vertices = [
+            (-1, -1, -1), (1, -1, -1), (1,  1, -1), (-1,  1, -1),
+            (-1, -1,  1), (1, -1,  1), (1,  1,  1), (-1,  1,  1)
+        ]
+
+        hexa = overlap.Hexahedron(vertices)
+        s = overlap.Sphere((1, 1, 1), 1)
+
+        np.testing.assert_almost_equal(overlap.overlap(s, hexa), np.pi / 6)
+
+
+class TestOverlapArea:
+    def test_wedge(self):
+        vertices = [
+            (-1, -1, -1), (1, -1, -1), (1,  1, -1),
+            (-1, -1,  1), (1, -1,  1), (1,  1,  1),
+        ]
+
+        wedge = overlap.Wedge(vertices)
+        s = overlap.Sphere((0, 0, 0), 1)
+
+        np.testing.assert_almost_equal(overlap.overlap_area(s, wedge), [
+            s.surface_area / 2,
+            0., 0., 0., s.radius**2 * np.pi, 0.,
+            s.radius**2 * np.pi
+        ])
+
+    def test_hexahedron(self):
+        s = overlap.Sphere((1, 1, 1), 1)
+
+        vertices = [
+            (-1, -1, -1), (1, -1, -1), (1,  1, -1), (-1,  1, -1),
+            (-1, -1,  1), (1, -1,  1), (1,  1,  1), (-1,  1,  1)
+        ]
+
+        hexa = overlap.Hexahedron(vertices)
+
+        face_overlap = s.radius**2 * np.pi / 4
+
+        np.testing.assert_almost_equal(overlap.overlap_area(s, hexa), [
+            s.surface_area / 8,
+            0., 0., face_overlap, face_overlap, 0, face_overlap,
+            3 * face_overlap
+        ])