Conversation
Introduce ot.lp.emd_grid_l1(A, B), an exact solver for the Earth Mover's Distance with the cityblock ground metric between histograms sharing a d-dimensional Cartesian grid support. Rather than solving the min-cost flow on the full bipartite graph (as ot.emd would), this reduces the problem to a min-cost flow on the much sparser grid adjacency graph following Ling & Okada (2007), then hands it to POT's existing off-the-shelf network simplex LP solver instead of their bespoke tree-based one. - New ot/lp/sparse_digraph.h: a general (non-bipartite) sparse digraph, needed because every grid cell can carry both supply and demand, unlike the existing bipartite sparse_bipartitegraph.h. - New EMD_wrap_grid_l1 in EMD_wrapper.cpp/EMD.h: builds the grid graph from a shape array, runs NetworkSimplexSimple, and decomposes the resulting flow into a transportation plan (including same-bin "self-mass" that already overlaps between A and B, which the flow decomposition alone would miss). - New ot/lp/_grid.py: the Python-facing emd_grid_l1, with a dedicated 1D fast path (POT's own emd_1d_sorted for the plan; a closed-form, fully backend-native O(n) reduction for the cost-only case, so a 1D GPU array never leaves the device). Backend-compatible throughout; the sparse plan is returned as G via the backend's coo_matrix, same convention as ot.emd2_lazy's return_matrix. - test/test_grid.py: correctness against the dense solver, plan/coupling marginal checks, backend round-trips, and the 1D dispatch. Deliberately scoped to L1 only, with no gradient support yet -- see the PR description for benchmarks and open questions on L2 and batching. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #863 +/- ##
==========================================
+ Coverage 96.85% 96.89% +0.04%
==========================================
Files 128 130 +2
Lines 26197 26584 +387
==========================================
+ Hits 25373 25759 +386
- Misses 824 825 +1 🚀 New features to boost your workflow:
|
test_emd_grid_l1_1d_direct_plan_helper_mass_mismatch exercises the mass-mismatch early return in _emd_grid_l1_1d_plan, which codecov flagged as uncovered. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ckends Every other array-creation helper (zeros/ones/full) respects type_as for both dtype and device, but arange() ignored it entirely for dtype (and for device on Numpy/Jax/Tensorflow/Cupy). Fixes PythonOT#864. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expose the dual potentials alpha/beta (the gradient of cost w.r.t. A, B)
in log, via LEMON's network-simplex node potentials for d >= 2 (a free
byproduct of the solve) and a closed form for 1D grids. Add a grad
argument ('envelope' by default, or None) to control whether the
non-free 1D gradient pass runs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to discussion #862: adds
ot.lp.emd_grid_l1(A, B), an exact solver for the Earth Mover's Distance with the cityblock (L1) ground metric between two histograms sharing the samed-dimensional Cartesian grid support.Instead of solving the min-cost flow on the full bipartite graph between the
n = prod(A.shape)source and target bins (asot.emd/ot.emd2would), this reduces the problem to a min-cost flow on the much sparser grid adjacency graph (O(d*n)arcs instead ofO(n^2)), following the graph formulation of Ling & Okada [1]. Unlike that paper, which introduces a bespoke tree-based solver for the reduced graph, this reuses POT's existing (off-the-shelf)NetworkSimplexSimpleLP solver on it — as suggested in the discussion, this is a small, well-contained addition on top of infrastructure POT already ships.[1] Ling, H., & Okada, K. (2007). An efficient earth mover's distance algorithm for robust histogram comparison. IEEE TPAMI, 29(5), 840-853.
What's in this PR
ot/lp/sparse_digraph.h: a new general (non-bipartite) sparse digraph. The existingsparse_bipartitegraph.h(from [MRG] Sparse emd implementation #778) splits nodes into a source half and a target half; that doesn't work here since every grid cell can carry both supply and demand at once.EMD_wrap_grid_l1inEMD_wrapper.cpp/EMD.h: builds the grid adjacency graph directly from ashapearray, runsNetworkSimplexSimpleon it, and (only when requested) decomposes the resulting Beckmann-style arc flow into an explicit transportation plan — including the "self-mass"AandBalready share at the same bin, which the flow decomposition alone misses (more on this below).ot/lp/_grid.py(emd_grid_l1): the Python-facing entry point.A,Bare passed as actuald-dimensional arrays (not flattened + a separate shape argument) so they naturally carry their own grid geometry.return_plan=Falseby default: recovering the plan has a real cost of its own (network simplex flow decomposition, or an O(n) merge in 1D), so it's opt-in.emd_1d_sorted, skipping the network-simplex setup entirely;nx.cumsum/nx.abs/nx.sum) and no CPU round-trip at all, verified end-to-end on an MPS GPU tensor.ot.backend). The general (ndim >= 2) path and the 1D-with-plan path do need a CPU round-trip for the compiled solver, same asot.emd/ot.emd2_lazy.log["G"], a sparse matrix built via the backend'scoo_matrix— the same mechanism and conventionot.emd2_lazy'sreturn_matrixalready uses (real sparse type for NumPy/PyTorch/TensorFlow/CuPy, densified for JAX, which has no sparse array type).test/test_grid.py: correctness against the dense solver on random grids (1D-4D), plan/coupling marginal checks (row/col sums ofGmatchA/Bexactly, not just the net residual), backend round-trips (including dtype/device preservation), and dispatch checks (mocking confirms the 1D path never touches the general C++ solver, and the plan-less 1D path never touches the O(n) merge either).Benchmarks
Script attached at the bottom of this description (
local_sandbox/bench_grid_l1.py, not committed). Comparesemd_grid_l1againstot.emd2(dense) andot.emd2_lazyforndim2-4, and againstot.emd2_1d(POT's own dedicated 1D solver) forndim=1, across a range of grid resolutions. All costs match exactly wherever compared (asserted in the script); a couple of the largest dense/lazy runs needednumItermaxraised from the default 100000 to 2,000,000 to actually converge on the full bipartite graph at that size (noted below).Notes:
-entries: dense/lazy skipped aboven=2000nodes to keep the benchmark's runtime bounded (full bipartite network simplex gets slow, and denseMbecomes memory-prohibitive);emd_grid_l1is still timed there to show scaling. The(3, 30)row (27,000 nodes) mirrors the 30×30×30 example from the discussion.emd2_1dis modest (1.0x-3.5x) rather than the 1000x+ margin seen against the generic bipartite solvers — expected, sinceemd2_1dis already the rightO(n log n)tool for a shared sorted support, andemd_grid_l1's own 1D plan path literally calls the same underlying routine. The gap that remains isemd_grid_l1's closed-form, network-simplex-free cost-only path.ndim >= 2, speedup over dense/lazy grows withnas expected, up to ~150x/300x atnin the low thousands.Deliberately out of scope for this PR
This PR is scoped to L1 only, with
nogradient support. If this direction is OK for integration, natural follow-ups would be:(d+1)-partite graph construction of Auricchio et al. (also discussed in Faster exact EMD for distributions on shared multi-dimensional cartesian grids (with cityblock or sqeuclidean ground metric) #862), as a separate solver alongside this one.Gradient support, most likely via the envelope theorem using dual potentials ([EDIT: Gradient computation now supported]net.potential()isn't currently plumbed throughEMD_wrap_grid_l1), mirroring howot.emd/ot.emd2_lazydo it.Question: batched support?
Should
emd_grid_l1support a batch dimension (e.g.A,Bof shape(B, n_1, ..., n_d))? The generic exact solvers in POT (ot.emd/ot.emd2/ot.emd2_lazy) don't batch — network simplex is a sequential pivoting algorithm per problem, so there's no way to vectorize it the way Sinkhorn/proximal iterations are batched inot.batch. That's still true here: the Ling & Okada reduction is inherently per-pair and CPU-oriented. But since each pair in a batch is fully independent, one option specific to this solver would be to parallelize across the batch in C++ (e.g. an OpenMPparallel forover batch entries, each running its ownNetworkSimplexSimpleinstance), rather than trying to vectorize within a single solve. Happy to prototype this as a follow-up if there's interest, but wanted to flag it now rather than bake in an API that doesn't anticipate it.Test plan
pytest test/test_grid.py(32 tests: correctness vs. dense solver on 1D-4D grids, plan/coupling marginals, backend round-trips incl. GPU, dispatch mocking)ot/lp/_grid.pypytest test/, excluding slow gromov/unbalanced/sliced/batch dirs): 1719 passed, no regressionsruff check/ruff formatcleanot.emd2/ot.emd2_lazy/ot.emd2_1don random grids up to 27,000 nodes (see benchmarks above)Benchmarking script (not committed, for reference)
local_sandbox/bench_grid_l1.py🤖 Generated with Claude Code