Background
This page builds intuition for what SDOT computes, how the solvers work, and what quantities you can extract from an OT plan.
The base problem
Given:
- f — a discrete measure: a finite collection of weighted Dirac masses at positions
with masses (with ) - g — a continuous density on some domain
Find the transport map
There are variants for the ground metric, notably for periodicity (see ground metrics).
Non euclidean norms are also planed (see roadmap)
Power Diagrams (Laguerre Cells)
The optimal transport map in the semi-discrete setting is always a power diagram (also called a Laguerre tessellation): a partition of
When all weights
The transport plan then simply sends every point
The Newton Solver
SDOT finds the optimal weights by solving:
The weights maximize a concave function, whose gradient is the vector of the mass errors: a damped Newton (+ some specific optimizations) method converges in a few iterations to a few dozen. The key cost is computing the power diagram and its integrals at each step, which runs in O(n log n) time.
Reference: Kitagawa, Mérigot, Thibert — Convergence of a Newton algorithm for semi-discrete optimal transport, JEMS 2019.
Computing a Plan
Functions like sdot.w2 (for the 2 norm) give only the distance. If you want more information, you can use sdot.plan (with some default values)
p = sdot.plan( f, g )
p.cost # W2^2 (by default)
p.barycenters # the barycenter of each cell, [ n, d ]
p.weights # the weights w of the power diagram, [ n ]
p.power_diagram() # the geometrySee plans and settings.
Barycenters and the Lloyd Algorithm
The barycenters p.barycenters[ i ] are the centroids of each transport cell:
Here is an example of use of the barycenter, in a Lloyd algorithm : moving each Dirac to its barycenter and repeating gives converges to an optimal quantization of
import numpy as np
import sdot
rng = np.random.default_rng( 0 )
positions = rng.random( ( 100, 2 ) )
pb = sdot.OtProblem( sdot.SumOfDiracs( positions ), sdot.Image( rng.random( ( 10, 10 ) ) ) )
for _ in range( 10 ):
p = pb.solve() # restarts from the previous solution by itself
positions = p.barycenters
pb.source = sdot.SumOfDiracs( positions )Actually, if you are intersted in optimal quantization, sdot.quantization does it for you: see the Lloyd quantization example (the snippet is here to show how to get the barycenters).