A modal spectral method for the Eikonal equation applied to continuous shortest path problems on simplicial tesselations.
The primarily goal in developing this library is for use in solving continuous shortest path problems, with application in navigation, infrastructure design, and GR. This README is more of a stream of conciousness blogging of progress, obstacles, solutions, and new ideas, all as they relate to this project.
The implementation offers a backend library for computing near-optimal Gaussian-like quadratures on the simplex in dimensions 1,2, and 3 (see ngjquad.cpp for driver code), built on top of NLOPT, BLAS and LAPACK, my own work, theoretical results due to Koornwinder regarding a family of orthogonal polynomials on the triangle, and numerical results due to Vioreanu and Rokhlin on constructing near-optimal Gaussian-like quadratures over convex regions (VR-quadrature).
I term the VR-quadrature approach "Discretize-Orthogonalize-Optimize" (DOO), while my approach is more "Orthogonalize-Discretize-Optimize" (ODO). This nomenclature draws analogy from similar jargon in variational/infinite-dimensional optimzation - Discretize-then-Optimize (DtO) or Optimize-then-Discretize (OtD) - as both concern a choice of an analytical or numerical first step.
The generatable quadrature rules are used to represent to high accuracy and with high efficiency functions appearing in Eikonal models under the Koornwinder polynomial basis family.
Recent results by Townsend, Oliver and Vasil enable the construction of sparse differential operators on the standard triangle, interoperating within the Koornwinder polynomial family parameter space, and leading to low-storage, banded discrete differential operators. The library provides an implementation of these discrete PDOs. They can be composed together to form solvers for a variety of linear PDEs of arbitrary order, even with variable coefficients (coming soon (i hope)). Non-linearity is handled by passing an appropriately defined problem to nlopt in terms of these operators acting on minimization variables (solution coefficients in a Koornwinder expansion).
At the time of writing, the following capabilities exist:
-
representing and manipulating functions in a modal sense under Koornwinder polynomial expansions (global approximation) as well the so-called Jacobi polynomials on the Tetrahedron.
-
differentiation up to any order via promotion and ladder operators acting only on modes of a function on the triangle
-
evaluation at a point in real space of a function (dim=1,2,3) represented in the modal basis (coefficients). This could be accelerated with Clenshaw's algorithm, generalized to 2D and 3D
-
Implementation of analytical entries to the Jacobi matrices appearing in the 2D-recurrence relation for the orthogonal Koornwinder polynomials, generalized for any choice of parameters. The 3D ones remain to be derived.
-
Quadrature discovery with optimization parameters from
nloptexposed for such use. In particular, I developed an interlaced scheme where first a constrained non-linear optimization problem is solved, then, if needed, a Newton relaxation, followed by another non-linear problem with a different objective, and a last on-demand Newton relaxation.- The gist is:
With
z=(x,y,w)inR^(3N), minimize overznorm(P(x,y).w - e1)^2, wheree1is the first basis vector inR^MwithN < M,P(x,y)is theNxMVandermonde matrix,(x,y)is constrained to lie within the triangle, andsum(w)is constrained to 1, followed by a Newton relaxation to find roots ofP(z).w - e1 = 0. Then the argmin is passed to another constrained non-linear problem, wherein we try to minimizecond(P(x,y))(in the2-norm) withwfixed, and constrainnorm(P(x,y).w-e1)to be at most the objective minimum from the first optimization problem. This seems to find Gaussian-like quadrature rules at the limit proposed by Vioreanu and Rokhlin (in terms of how muchN < M), but with the added benefit ofO(1)condition number for the interpolation matrix. This latter feature is important for modal discretizations of PDEs, as when we impose boundary conditions nodally, the interpolation operator on those nodes must be well conditioned if we hope for the assembled systems to be invertible.
- The gist is:
With
For example, the image below depicts the output when attempting to generate an N = 253 (n max = 21) node quadrature rule which can exactly integerate 630 polynomials (m max = 34). In this case, we use the Subplex algorithm, strict tolerances of 1e-14 on the relative change in iterates of the minimization varialbes, and 6 threads.
The red nodes are our new found abscissa, while the black ones are the initial points. We see the new nodes are well conditioned, and we correctly integrate the test function
sin(x^2+y^2) over the standard right triangle with vertices (0,0), (0,1), (1,0) (plug it into Wolfram alpha for reference). See triaintq.f for comparison at https://github.com/zgimbutas/triasymq. We exceed the largest rule generated there, and ours is better conditioned by 2 two orders of magnitude.
There remains much to be done for this project, on both paper and metal!
We seeks roots to the function F(x,y,w) = P(x,y).w - I = 0, where P is the transpose of the vandermonde matrix P(x,y)^T, and (x,y) are a set of points in the standard right triangle. There is some theoretical limit on the solvability of the equation in terms of the number of points (#columns of P) and the number of integrals for which we want exact quadrature (#rows of P or length of I). We know it is not solvable at the optimal Gaussian quadrature limit, as the Jacobi matrices for Koornwinder polynomials on the simplex are not a commuting family. The limit is explored numerically for more general convex regions by Rokhlin and Vioreanu, and an empirical cutoff formula is provided in their work. Assuming we are under this limit in terms of the system size, and further assuming a root exists (ignoring conditioning of P), then ||F||^2 in the 2-norm should also admit a zero at some point (x,y,w). Rather than trying to minimize ||F||^2 using non-linear optimization techniques like SQP, or any method where quadratic approximations to sub-problems are used, we might be as well successful building a quadratic approximation of the objective G=||F||^2 and using the Newton direction -(\nabla^2 G(x,y,w))^{-1}\nabla G(x,y,w) to find the root. The only "approximation" to be made here in the model is for the gradient and hessian entries concerning variable w. We can hopefully use simple centered finite difference schemes there. Variables x,y enter only in P, and we should know by now how to take derivatives of polynomials. Ill-conditioning in the argmin can be handled with our current approach (nlopt call on min cond(P)).
- the first step is seeing if incorporating analytical gradient information even helps convergence in the
nloptroutines currently used. After all, if it isn't broken, don't fix it, and we are already able to generate high order quadrature with a large increase in efficiency relative to the past (days->hours on the same OTS equipment). I will need to check if the ones currently in use even need derivatives (many are derivative-free algorithms), and whether there are others that require derivative info. - if
nloptresponds positively to analytical gradient info, then we can just plug and play the Hessian into my current Newton method implementation, discarding the backtracking line search part. Let's see what happens!
Newton's method has fast quadratic convergence, but there is difficulty of step length control for gradient descent which leads to constraint violations for the quadrature rule. If we can get Newton descent right, it may open up the possibility for on-line p-adaptivity in PDE discretizations using the Koornwinder bases (which also reminds me of the possiblity of (a,b,c) adaptivity - interoperating between basis families parameterized by (a,b,c)).
The only challenge with all of this has been in deriving matrix entries for the 3-term matrix recurrence relations defining spaces of polynomials orthogonal to all spaces of lower order (for d > 1). We need these entries to assemble Jacobi matrices, and we need those to initialize any descent method for finding Gaussian-like quadrature/cubature. By definition, Jn_i.P = x_i.P + [0\\A_{n-1}_i \bb{P}_n]. In 2D, if {x_1,x_2}_j is a set of nodes admitting a well conditioned interpolation matrix on the triangle, then we should be able to solve to mach_eps for the entries in Jn_1,Jn_2. All we would need to do so (and, in fact, already have) are the entries to A_{n-1}_{1,2}. In higher dimensions, this reduces the courageous pencil/paper work by quite a bit, even if we use symbolic algebra systems. Then, we can consider JEVD techniques used in SP problems like BSS to generalize the initialization method for d>2 (complexification doesn't work for 3D, which is used in 2D for the eigenvalue matching problem. Maybe we can use quaternions and projection to dim under for 3D? Is there even an eigenvalue solver that supports quaternions??).
Consider orthogonal polynomials up to total degree
That is, we claim that there exists a single unitary matrix
Solve
constrained by
where
The idea is to throw this into an optimization routine on vectorizations of the minimization variables, so there are
- Ensure the constraint is satisfied to begin with by setting
$U=I$ .- Try initializing
$F^k$ with a perturbation of$J^k$ drawn randomly from a distribution to be determined. - Try initializing
$F^k = (J^k)_{ii}$ the diagonal elements of$J^k$ .
- Try initializing
- Since we seek an 'average eigenstructure' for
$(J^k)$ , initialize the QR decomposition of one of the$J^k$ .
A better idea would be to use an old, but working implementation (Jacobi angles for simultaneous diagonalization), and port it into the library with threading/simd additions. In fact,
this has already been implemented with simd optimizations in the library, and tested successfully (see include/jevd.h and gtest code for testing). There are some challenges with threading the algorithm, mainly to do with an appropriate
packing of the input matrices into a combined matrix so as to minimize cache incoherencies, and maximize the number of loops the compiler deems vectorizable. There's also some more thinking that
needs to be done in terms of restructuring the algorithm to avoid race conditions on matrix updates, but not require any critical barriers on execution per thread. For now, I'm happy with
a serial but vectorized code that can handle
UPDATE: It works! JEVD can be used to initialize nodes for Gaussian-like quadrature optimization on the Tetrahedron. See the image below, which I generated for n=5. Implementation of the
definitions for the Jacobi polynomials on the tetrahedron remains, along with the structural constants, Jacobi matrix block defintions, etc (UPDATE: all that is done too). However, I have written procedures in
Mathematica to symbolically generate most of this stuff. I need to tweak that code a bit so that it gives me nicer formulas, which I can then just port into C (I rly don't like algebra).
The jacobi matrices, the approximate joint eigenvalues of which appear as the nodes above, have interesting expanding block band sparsity patterns (
Since I've been waiting for 2 days on the Wolfram Kernel (using 90% of my computer's memory) to evaluate Jacobi matrix entries on the tetrahedron for n = 15, I had to do some thinking..
I realized there exists a straightforward way of numerically computing the entries to the Jacobi matrices for the tetrahedron. However, it relies on getting the near Gaussian-like quadrature optimization working correctly. The idea is to bootstrap:
- Generate Jacobi matrices for a low order, like n = 4.
- Generate the initial nodes for adapting the quadrature rule via JEVD
- Push this adapatation as far as we can, resulting in a higher order rule using the same number of nodes.
- Use this new rule to approximate the required weighted inner products for entries to the Jacobi matrix, but for a higher order, like n = 5 (if we started with n = 4).
- Repeat this process until we get SUPER high order!!
The justification here is that we will certainly be able to compute the required integrals for
the next order, to within some relatively acceptable (but still unacceptable) epsilon. Now, if
this epsilon error is small enough, the JEVD process should still produce nodes that live in
the tetrahedron, and would serve well for initializing the quadrature optimization routine.
Conditioning is not a concern here. We only want to integrate accurately, so
we can eventually have accurate interpolation and function expansion. Think of the epsilon in
terms of the epsilon-commutativity of the Jacobi matrices. We never really find exact joint
eigenvalues, as they don't exist in this case!
We only find the joint eigenvalues of
Remark: I'm assuming that this epsilon commuting family exists. That is, I assume the Jacobi matrices live on some kind of manifold of non-commuting matrices, but are just far enough away from a point on a manifold of commuting matrices. So, I assume that we need only push these matrices a small amount to be on the manifold of commuting matrices, and their projections on that manifold yield eigenvalues that live in the tetrahedron. The vaguery of language here is intentional. The point is that we should still be able to push the matrices to the point with eigenvalues which are in a basin of convergence for the underdetermined linear system / quadratic root finding problem we seek to solve at the end of the day.
(UPDATE:) EHHHH!!! - we can't bootstrap because the inner products we need to compute involve order 2n+1 total degree polynomials. BUT, we can kinda-sorta bootstrap by incorporating the below process, when said process starts lacking in efficacy.
(FIX THE WRONG) We can use mapped quadrature rules to achieve the end goal of computing these inner products.
I have defined a deformation mapping between the unit cube (in R3+) and the standard-right tetrahedron.
By making use of the fact that in C2T :
$ \int_T f(y) dy= \int_{[0,1]^3} f(C2T[x]) det[\nabla_x C2T]dx$
where the det term is the Jacobian determinant, or change of variable form. The utility here is that
we have recast an integral over a domain for which we do not have a sufficiently accurate quadrature
rule into an integral over a domain for which we can use a tensor product of simple 1D quadrature rules.
In my case, I just generate the (0,0) Jacobi polynomials (i.e. Legendre basis), and use the Golub-Welsch
algorithm to compute an optimal Gaussian quadrature rule of order
(UPDATE:) This worked swimmingly, and I can now generate high order quadrature on the tetrahedron relatively quickly! It turns out that the error in numerically approximating the inner products is within the tolerable error required for JEVD routines on the approximate Jacobi matrices to converge. There are research cookies here in terms of proving the relationship between the integration error, the off-diagonality minimization error, and interiority of matched eigenvalues to the simplex. I'll leave it to a numerical analyst to prove this stuff, though the relations are readily observable in numerical experiments.
I've moved on to making the library routines callable from Python, and playing around with solving Eikonal problems on the triangle. I migrated from plain-old make to CMake as the build system, which has made compilation a lot faster, and I guess I was linking things incorrectly, so the code runs faster too! I can add support for the Intel icc compilers too, but don't have the free version yet, so haven't bothered.
It is likely most simple to install and use the libarary from within a Docker container.
For user convenience, a Dockerfile is provided which can generate a Docker image with
all required dependencies and the Eikonal library installed. Note, the Docker Engine must
be installed on your system (See https://docs.docker.com/engine/), along with git.
Administrative privileges may be required to run Docker commands. On Linux systems, you must add your user to the Docker group by executing:
sudo groupadd docker
sudo usermod -aG docker <user> # replace <user> with your username
newgrp docker # or log out and log back inSince you need root privilege to execute the above commands, you could instead forgo their execution, and prefix docker commands with sudo.
Once these dependencies are met, executing (on Linux or Unix systems) the following commands will
build the Docker base image for the project and test the installation within a running
instance of the image (a Docker container):
git clone git@github.com:snatesh/Eikonal.git
cd Eikonal
docker build -t ngj_tri_opt:latest .
docker run -it ngj_tri_opt:latest bash
cd Eikonal
cd build && ctest --verboseAbove, the docker run command with bash post-fixed will instantiate a
bash shell running in the container, within which you can read/write/execute
files as you please. The last two commands above change directory into
the /Eikonal folder in the container context, compile the
gtest code (located in /Eikonal/testing/gtest.cpp), and run the tests therein.
g++compiler (tested on V13.2.0)build-essentials(particularly GNUmakeutility)- google testing framework (available via dpkg and apt as
libgtest-dev) dh-autoreconf,autoreconf,autotoolsforSNOPTinstallation (currently not used)NLOPT(open source non linear optimization library,https://github.com/stevengj/nlopt.git)cblasandlapack- Installation is easiest via package manager as:
sudo apt install libopenblas-openmp-dev liblapacke-devwhich ensures the C wrapper to lapack (in headers lapacke.h) is
installed in a sane location.
-
multipledispatchlibrary inPython. -
The c++ compiler should have support for the
OpenMPshared memory parallelization library, and the library must exist on your system. That is,omp.handlibomp.somust exist somewhere in the filesystem, the compiler must understandOpenMPdirectives, and the linker should be able to find and link tolibompgiven the-fopenmpflag. Most modern compilers will ship with the header and library files, as well as support/implementation ofOpenMPdirectives. In case the files don't make it, you can use (onLinuxwithdpkg)
sudo apt install libomp-devAn example threading config file cpuconfig.sh is included to show some of the environment variables that OpenMP exposes
for users to set from the shell. In general, using the number of physical cores on the system improves the performance of
most algorithsm that Eikonal uses (over hyperthreading), while the binding of threads spawned by OpenMP to those physical core IDs is
something with which you should experiment on your system by setting the OMP_PROC_BIND and OMP_PLACES environement variables.
Proc-binding essentially disables hyperthreading when you set the number of threads to less than or equal to the number of cores per socket.
I find that enabling threads significantly reduces the convergence time for quadrature search when n,m are large enough (>~10), so using and playing around with OpenMP settings is well worth the effort if you need high order quadrature.
# number of threads for OpenMP
num_threads=6
# let the shell use the maximum amount of stack memory
ulimit -s unlimited
# set mem for thread stack
export OMP_STACKSIZE=256m
#thread pinning settings
export OMP_PLACES="{0}:${num_threads}:1"
export OMP_PROC_BIND=true
export OMP_DISPLAY_ENV=true
export OMP_NUM_THREADS=${num_threads}Note, the OMP_NUM_THREADS variable is set here, though this will change. It is not advisable to set such an environmental variable if you link to other programs which also use OpenMP. They may have their own tested/working heuristics for setting the number of threads. and fixing it in the shell context can mess up the performance of their threaded functions when called from within the same context. The num_threads variable set above will eventually be passed to the program at runtime (set by calling omp_set_num_threads), while OMP_NUM_THREADS will be unset/empty.
The wrapper libraries are compiled by default, and the Python bindings are available in the python folder. You have to
set the relevant environment variables so the interpreter can link to the libraries declared in the bindings files. From the Eikonal directory, run
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:$PWD/lib
export PYTHONPATH=${PYTHONPATH}:/usr/local/lib/python3.12/site-packagesensuring to replace the location of your Python installation accordingly. If using Docker, this is handled on image creation.
The open-source nonlinear optimization libary which we use is nlopt, by our favorite FFTW co-creator Steven Johnson!
For our purposes, it is used to generate near-optimal Gaussian-like quadrature on the triangle, and is to be investigated for
use in solving non-linear PDEs/PDE-constrained optimization problems.
To install NLOPT, simply execute
git clone https://github.com/stevengj/nlopt.git
cd nlopt
mkdir build && cd build
cmake ..
make
sudo make isntall which (on Linux systems) will by default install header and shared library files in
/usr/local/include and /usr/local/lib.
Following the discussion of threading with OpenMP above, it seems that NLOPT is threaded using the lower level pthreads library, but responds to the OMP_NUM_THREADS variable setting (i.e. if num_threads=6, CPU utilization will not exceed 600%). I have yet to test whether this response is in favor of performance, in terms of time or memory use. The faster we can go, the farther we can push the order of generated quadratures, so this is worth looking into at some point.




