Tutorial

In this tutorial, we consider two ellipsoids, and check wether or not they overlap.

Ellipsoid E₁ is an oblate spheroid centered at point x₁ = (-0.5, 0.4, -0.7), with equatorial radius a₁ = 10, polar radius c₁ = 0.1 and polar axis (0, 0, 1).

Ellipsoid E₂ is a prolate spheroid centered at point (0.2, -0.3, 0.4), with equatorial radius a₁ = 0.5, polar radius c₁ = 5 and polar axis (1, 0, 0).

To carry out the overlap check, we must first create the representation of ellipsoids Eᵢ as quadratic forms Qᵢ (see Mathematical representation of ellipsoids). Convenience functions are provided to compute the matrix representation of a spheroid.

Note

In principle, the contact function implemented in PW85 applies to any ellipsoids (with unequal axes). However, at the time of writing this tutorial (2019-01-01), convenience functions to compute the matrix representation of a general ellipsoid is not yet implemented. Users must compute the matrices themselves.

We first check for the overlap of E₁ and E₂ using the Python wrapper of pw85. We will then illustrate the C API.

Python tutorial

The Python module relies on NumPy for passing arrays to the underlying C library. We therefore import both modules:

>>> import numpy as np
>>> import pw85

and define the parameters of the simulation:

>>> x1 = np.array([-0.5, 0.4, -0.7])
>>> n1 = np.array([0., 0., 1.])
>>> a1, c1 = 10, 0.1
>>> x2 = np.array([0.2, -0.3, 0.4])
>>> n2 = np.array([1., 0., 0.])
>>> a2, c2 = 0.5, 5.
>>> r12 = x2-x1

where r₁₂ is the vector that joins the center of the first ellipsoid, x₁, to the center of the second ellipsoid, x₂.

We use the function pw85.spheroid() to create the matrix representations q₁ and q₂ of the two ellipsoids. Note that these arrays must be preallocated:

>>> q1 = np.empty((6,), dtype=np.float64)
>>> pw85.spheroid(a1, c1, n1, q1)
>>> q1
array([ 1.e+02, -0.e+00, -0.e+00,  1.e+02, -0.e+00,  1.e-02])
>>> q2 = np.empty_like(q1)
>>> pw85.spheroid(a2, c2, n2, q2)
>>> q2
array([25.  ,  0.  ,  0.  ,  0.25,  0.  ,  0.25])

We can now compute the value of the contact function — see the documentation of pw85.contact_function():

>>> out = np.empty((2,), dtype=np.float64)
>>> pw85.contact_function(r12, q1, q2, out)
>>> mu2, lambda_ = out
>>> print('μ² = {}'.format(mu2))
>>> print('λ = {}'.format(lambda_))
μ² = 3.362706040638343
λ = 0.1668589553405904

We find that μ² > 1, hence μ > 1. In other words, both ellipsoids must be swollen in order to bring them in contact: the ellipsoids do not overlap!

Checking the output

The output of this simulation can readily be checked. First, we can check that q₁ and q₂ indeed represent the ellipsoids E₁ and E₂. To do so, we first construct the symmetric matrices Q₁ and Q₂ from their upper triangular part

>>> Q1 = np.zeros((3, 3), dtype=np.float64)
>>> i, j = np.triu_indices_from(Q1)
>>> Q1[i, j] = q1
>>> Q1[j, i] = q1
>>> Q1
array([[ 1.e+02, -0.e+00, -0.e+00],
       [-0.e+00,  1.e+02, -0.e+00],
       [-0.e+00, -0.e+00,  1.e-02]])
>>> Q2 = np.zeros_like(Q1)
>>> Q2[i, j] = q2
>>> Q2[j, i] = q2
>>> Q2
array([[25.  ,  0.  ,  0.  ],
       [ 0.  ,  0.25,  0.  ],
       [ 0.  ,  0.  ,  0.25]])

We can now check these matrices for some remarkable points, first for ellipsoid E₁

>>> Q1_inv = np.linalg.inv(Q1)
>>> f1 = lambda x: Q1_inv.dot(x).dot(x)
>>> f1((a1, 0., 0.))
1.0
>>> f1((-a1, 0., 0.))
1.0
>>> f1((0., a1, 0.))
1.0
>>> f1((0., -a1, 0.))
1.0
>>> f1((0., 0., c1))
0.9999999999994884
>>> f1((0., 0., -c1))
0.9999999999994884

then for ellipsoid E₂

>>> Q2_inv = np.linalg.inv(Q2)
>>> f2 = lambda x: Q2_inv.dot(x).dot(x)
>>> f2((c2, 0., 0.))
1.0
>>> f2((-c2, 0., 0.))
1.0
>>> f2((0., a2, 0.))
1.0
>>> f2((0., -a2, 0.))
1.0
>>> f2((0., 0., a2))
1.0
>>> f2((0., 0., -a2))
1.0

Note that in the above tests, we have omitted the centers of ellipsoids E₁ et E₂ (both ellipsoids were translated to the origin).

We will now verify the corectness of the value found for the scaling factor μ. To do so, we will find the coordinates of the contact point of the two scaled ellipsoids, and check that the normals to the two ellipsoids at that point coincide.

Although we use formulæ from the Theory section to find the coordinates of the contact point, x₀, it is not essential for the time being to fully understand this derivation. What really matters is to check that the resulting point x₀ is indeed the contact point of the two scaled ellipsoids; how the coordinates of this point were found is irrelevant.

From the value of λ returned by the function pw85.contact_function(), we compute Q defined by Eq. (10) in section Theory, as well as x = Q⁻¹⋅r₁₂

>>> Q = (1-lambda_)*Q1+lambda_*Q2
>>> x = np.linalg.solve(Q, r12)

From which we find x₀, using either Eq. (9a) or Eq. (9b) (and we can check that both give the same result)

>>> x0a = x1+(1-lambda_)*np.dot(Q1, x)
>>> x0a
array([ 0.16662271, -0.29964969, -0.51687799])
>>> x0b = x2-lambda_*np.dot(Q2, x)
>>> x0b
array([ 0.16662271, -0.29964969, -0.51687799])

We can now check that x₀ belongs to the two scaled ellipsoids, that we first define, overriding the matrices of the unscaled ellipsoids, that are no longer needed. We observe that if ellispoid Eᵢ is scaled by μ, then its matrix representation Qᵢ is scaled by μ², and its inverse Qᵢ⁻¹ is scaled by μ⁻².

>>> x0 = x0a
>>> Q1 *= mu2
>>> Q2 *= mu2
>>> Q1_inv /= mu2
>>> Q2_inv /= mu2
>>> x = x0-x1
>>> Q1_inv.dot(x).dot(x)
1.0000000000058238
>>> x = x0-x2
>>> Q2_inv.dot(x).dot(x)
0.9999999999988334

Therefore x₀ indeed belongs to both ellipsoids. We now compute the normal nᵢ to ellipsoid Eᵢ at point x₀. Since ellipsoid Eᵢ is defined by the level-set: (x-xᵢ)ᵀ⋅Qᵢ⁻¹⋅(x-xᵢ) = 1, the normal to Eᵢ is given by Qᵢ⁻¹⋅(x-xᵢ) (which is then suitably normalized)

>>> n1 = Q1_inv.dot(x0-x1)
>>> n1 /= np.linalg.norm(n1)
>>> n1
array([ 3.64031943e-04, -3.82067448e-04,  9.99999861e-01])
>>> n2 = Q2_inv.dot(x0-x2)
>>> n2 /= np.linalg.norm(n2)
>>> n2
array([-3.64031943e-04,  3.82067448e-04, -9.99999861e-01])

We find that n₁ = -n₂. Therefore, E₁ and E₂ are in external contact. QED

Follow this link to download the above Python script.

C++ tutorial

The Python interface to PW85 has been kept close to the undelying C++ API. The following C++ program (download source file) defines the two ellipsoids, then computes μ² and λ:

#include <iostream>
#include <array>

#include "pw85/pw85.hpp"

using Vec = std::array<double, pw85::dim>;
using SymMat = std::array<double, pw85::sym>;

int main() {
  Vec x1{-0.5, 0.4, -0.7};
  Vec n1{0., 0., 1.};
  double a1 = 10.;
  double c1 = 0.1;

  Vec x2{0.2, -0.3, 0.4};
  Vec n2{1., 0., 0.};
  double a2 = 0.5;
  double c2 = 5.;

  Vec r12;
  for (int i = 0; i < pw85::dim; i++) r12[i] = x2[i] - x1[i];

  SymMat q1, q2;
  pw85::spheroid(a1, c1, n1.data(), q1.data());
  pw85::spheroid(a2, c2, n2.data(), q2.data());

  std::array<double, 2> out;
  pw85::contact_function(r12.data(), q1.data(), q2.data(), out.data());
  std::cout << "mu^2 = " <<  out[0] << std::endl;
  std::cout << "lambda = " << out[1] << std::endl;
}

A CMakeLists.txt file is provided for the compilation of the tutorial using CMake. You can reuse it in one of your own projects (download):

cmake_minimum_required(VERSION 3.13)

project("tutorial" LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)

add_executable(${PROJECT_NAME} ${PROJECT_NAME}.cpp)

find_library(MATH_LIBRARY m)
if (MATH_LIBRARY)
    target_link_libraries(${PROJECT_NAME} INTERFACE ${MATH_LIBRARY})
endif()

find_package(Boost REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE Boost::headers)

target_include_directories(${PROJECT_NAME} PRIVATE "../../include")

cd into the cpp_tutorial subdirectory. The provided example program should be compiled and linked against pw85:

$ mkdir build
$ cd build
$ cmake ..
$ cmake --build . --config Release

An executable called tutorial should be present in the build/Release subdirectory. On execution, it prints the following lines to stdout:

mu^2 = 3.36271
lambda = 0.166859