Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ include ot/lp/full_bipartitegraph_omp.h
include ot/lp/network_simplex_simple.h
include ot/lp/network_simplex_simple_omp.h
include ot/lp/sparse_bipartitegraph.h
include ot/lp/sparse_digraph.h
include ot/partial/partial_cython.pyx
include ot/bsp/BSP-OT_header_only.h
include ot/bsp/bsp_wrapper.cpp
Expand Down
1 change: 1 addition & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#### New features

- Add `ot.lp.emd_grid_l1`, an exact EMD-L1 solver for histograms sharing a multi-dimensional Cartesian grid support. It solves a min-cost flow on the grid's adjacency graph rather than the full bipartite graph (Ling & Okada, 2007), which is one to two orders of magnitude faster for this case (PR #863)
- Use `ot.utils.check_marginal` (and shape-tuple support in `ot.utils.unif`) to fill and validate default marginals consistently across solvers (Gromov, low-rank, stochastic, barycenter, factored) (PR #856)
- Add stereographic spherical sliced Wasserstein distance in `ot.sliced.stereographic_sliced_wasserstein_sphere`, with its rotationally invariant extension (PR #836)
- Add Quasi-Monte Carlo sliced Wasserstein sampling (QSW/RQSW) via generalized
Expand Down
27 changes: 22 additions & 5 deletions ot/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -1292,7 +1292,10 @@ def ones(self, shape, type_as=None):
return np.ones(shape, dtype=type_as.dtype)

def arange(self, stop, start=0, step=1, type_as=None):
return np.arange(start, stop, step)
if type_as is None:
return np.arange(start, stop, step)
else:
return np.arange(start, stop, step, dtype=type_as.dtype)

def full(self, shape, fill_value, type_as=None):
if type_as is None:
Expand Down Expand Up @@ -1730,7 +1733,12 @@ def ones(self, shape, type_as=None):
return self._change_device(jnp.ones(shape, dtype=type_as.dtype), type_as)

def arange(self, stop, start=0, step=1, type_as=None):
return jnp.arange(start, stop, step)
if type_as is None:
return jnp.arange(start, stop, step)
else:
return self._change_device(
jnp.arange(start, stop, step, dtype=type_as.dtype), type_as
)

def full(self, shape, fill_value, type_as=None):
if type_as is None:
Expand Down Expand Up @@ -2237,7 +2245,9 @@ def arange(self, stop, start=0, step=1, type_as=None):
if type_as is None:
return torch.arange(start, stop, step)
else:
return torch.arange(start, stop, step, device=type_as.device)
return torch.arange(
start, stop, step, dtype=type_as.dtype, device=type_as.device
)

def full(self, shape, fill_value, type_as=None):
if isinstance(shape, int):
Expand Down Expand Up @@ -2787,7 +2797,11 @@ def ones(self, shape, type_as=None):
return cp.ones(shape, dtype=type_as.dtype)

def arange(self, stop, start=0, step=1, type_as=None):
return cp.arange(start, stop, step)
if type_as is None:
return cp.arange(start, stop, step)
else:
with cp.cuda.Device(type_as.device):
return cp.arange(start, stop, step, dtype=type_as.dtype)

def full(self, shape, fill_value, type_as=None):
if isinstance(shape, (list, tuple)):
Expand Down Expand Up @@ -3240,7 +3254,10 @@ def ones(self, shape, type_as=None):
return tnp.ones(shape, dtype=type_as.dtype)

def arange(self, stop, start=0, step=1, type_as=None):
return tnp.arange(start, stop, step)
if type_as is None:
return tnp.arange(start, stop, step)
else:
return tnp.arange(start, stop, step, dtype=type_as.dtype)

def full(self, shape, fill_value, type_as=None):
if type_as is None:
Expand Down
21 changes: 21 additions & 0 deletions ot/lp/EMD.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ int EMD_wrap_sparse(
double *beta_init // Initial dual variables for targets (warmstart)
);

int EMD_wrap_grid_l1(
int ndim, // Number of grid dimensions
int64_t *shape, // Grid shape (ndim entries)
double *X, // Source histogram, flattened C-order (prod(shape))
double *Y, // Target histogram, flattened C-order (prod(shape))
bool return_plan, // If false, skip decomposing the flow into a
// transportation plan and only compute cost
uint64_t *plan_sources_out, // Output: source bin index of each plan entry
uint64_t *plan_targets_out, // Output: target bin index of each plan entry
double *plan_values_out, // Output: mass moved by each plan entry
uint64_t *n_plan_entries_out,
uint64_t max_plan_entries,
double *alpha, // Output: node potentials / dual variables
// (n_nodes = prod(shape)); the Beckmann
// formulation uses a single graph (not
// bipartite), so there is one potential
// array, not alpha/beta; beta = -alpha
double *cost, // Output: total transportation cost
uint64_t maxIter // Maximum iterations for solver
);

int EMD_wrap_lazy(
int n1, // Number of source points
int n2, // Number of target points
Expand Down
268 changes: 267 additions & 1 deletion ot/lp/EMD_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@

#include "network_simplex_simple.h"
#include "sparse_bipartitegraph.h"
#include "sparse_digraph.h"
#include "EMD.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <unordered_map>
#include <vector>
Expand Down Expand Up @@ -202,6 +205,98 @@ inline bool extract_sparse_solution(
return true;
}

// An arc carrying positive flow, keyed by its head node. Used to decompose
// grid min-cost-flow arc flows (which move mass between *adjacent* grid
// cells) into a direct (source_bin, target_bin, mass) transport plan.
struct GridFlowEdge {
int head;
double flow;
};

// Decomposes multi-hop arc flows into direct (source, target, mass) entries.
//
// A min-cost flow on the grid adjacency graph reports how much mass crosses
// each arc between neighbouring cells, but a transport plan has to say which
// bin each unit of mass came from and where it ended up. This walks from
// each node with leftover supply along arcs that still carry flow until it
// reaches one with a deficit, records that path's bottleneck as one plan
// entry, and subtracts it from every arc on the path, repeating until no
// supply is left. `flow_adj` is consumed in place and `rem_supply` is taken
// by value, both as scratch.
//
// Returns false if the plan would exceed `max_plan_entries` entries.
inline bool decompose_grid_flows(
std::vector<std::vector<GridFlowEdge>>& flow_adj,
std::vector<double> rem_supply,
uint64_t* plan_sources_out,
uint64_t* plan_targets_out,
double* plan_values_out,
uint64_t* n_plan_entries_out,
uint64_t max_plan_entries
) {
const double eps = 1e-10;
const std::size_t n_nodes = flow_adj.size();
std::vector<std::size_t> ptr(n_nodes, 0);

for (std::size_t src = 0; src < n_nodes; ++src) {
while (rem_supply[src] > eps) {
std::vector<std::pair<std::size_t, std::size_t>> path_edges;
std::size_t cur = src;

while (true) {
if (rem_supply[cur] < -eps && cur != src) {
break;
}
auto& list = flow_adj[cur];
std::size_t p = ptr[cur];
while (p < list.size() && list[p].flow <= eps) {
++p;
}
ptr[cur] = p;
if (p >= list.size()) {
break;
}
path_edges.emplace_back(cur, p);
cur = static_cast<std::size_t>(list[p].head);
}

if (path_edges.empty()) {
break;
}

const std::size_t target = cur;
if (rem_supply[target] >= -eps) {
break;
}

double bottleneck = rem_supply[src];
bottleneck = std::min(bottleneck, -rem_supply[target]);
for (const auto& edge : path_edges) {
bottleneck = std::min(bottleneck, flow_adj[edge.first][edge.second].flow);
}

if (bottleneck <= eps) {
break;
}

for (const auto& edge : path_edges) {
flow_adj[edge.first][edge.second].flow -= bottleneck;
}
rem_supply[src] -= bottleneck;
rem_supply[target] += bottleneck;

if (*n_plan_entries_out >= max_plan_entries) {
return false;
}
plan_sources_out[*n_plan_entries_out] = static_cast<uint64_t>(src);
plan_targets_out[*n_plan_entries_out] = static_cast<uint64_t>(target);
plan_values_out[*n_plan_entries_out] = bottleneck;
++(*n_plan_entries_out);
}
}
return true;
}

} // namespace


Expand Down Expand Up @@ -453,7 +548,178 @@ int EMD_wrap_sparse(
return ret;
}

int EMD_wrap_lazy(int n1, int n2, double *X, double *Y, double *coords_a, double *coords_b,
int EMD_wrap_grid_l1(
int ndim,
int64_t *shape,
double *X,
double *Y,
bool return_plan,
uint64_t *plan_sources_out,
uint64_t *plan_targets_out,
double *plan_values_out,
uint64_t *n_plan_entries_out,
uint64_t max_plan_entries,
double *alpha,
double *cost,
uint64_t maxIter
) {
using namespace lemon;

int64_t n_nodes = 1;
for (int d = 0; d < ndim; ++d) {
if (shape[d] <= 0) {
return INFEASIBLE;
}
n_nodes *= shape[d];
}

double total_x = 0.0;
double total_y = 0.0;
bool any_diff = false;
for (int64_t i = 0; i < n_nodes; ++i) {
if (X[i] < 0 || Y[i] < 0) {
return INFEASIBLE;
}
total_x += X[i];
total_y += Y[i];
any_diff = any_diff || (X[i] != Y[i]);
}
if (std::abs(total_x - total_y) > 1e-8 * std::max(1.0, total_x)) {
return INFEASIBLE;
}

*cost = 0.0;
*n_plan_entries_out = 0;

if (!any_diff) {
// Histograms are identical: the cost is 0 and constant in a
// neighbourhood of X == Y, so the zero potential is a valid
// (sub)gradient here.
std::fill(alpha, alpha + n_nodes, 0.0);
// Nothing to transport, but if a plan is requested, the identity
// coupling is still the (trivially optimal) transportation plan.
if (return_plan) {
for (int64_t i = 0; i < n_nodes; ++i) {
if (X[i] > 1e-10) {
if (*n_plan_entries_out >= max_plan_entries) {
return (int)MAX_ITER_REACHED;
}
plan_sources_out[*n_plan_entries_out] = static_cast<uint64_t>(i);
plan_targets_out[*n_plan_entries_out] = static_cast<uint64_t>(i);
plan_values_out[*n_plan_entries_out] = X[i];
++(*n_plan_entries_out);
}
}
}
return OPTIMAL;
}

// Grid-adjacent arcs: one forward and one backward arc per adjacent cell
// pair, unit cost each. On a unit-spaced Cartesian grid this reduces the
// cityblock-EMD problem to a min-cost flow on the grid graph, which is
// far sparser than the full bipartite graph (Ling & Okada, 2007). Unlike
// that paper's bespoke tree-based solver, the reduced graph below is
// handed to the off-the-shelf NetworkSimplexSimple LP solver.
std::vector<int64_t> stride(ndim);
stride[ndim - 1] = 1;
for (int d = ndim - 2; d >= 0; --d) {
stride[d] = stride[d + 1] * shape[d + 1];
}

std::vector<std::pair<int, int>> edges;
for (int d = 0; d < ndim; ++d) {
const int64_t extent = shape[d];
if (extent < 2) {
continue;
}
const int64_t st = stride[d];
for (int64_t u = 0; u < n_nodes; ++u) {
if ((u / st) % extent < extent - 1) {
edges.emplace_back(static_cast<int>(u), static_cast<int>(u + st));
edges.emplace_back(static_cast<int>(u + st), static_cast<int>(u));
}
}
}

typedef SparseDigraph Digraph;
Digraph di(static_cast<int>(n_nodes));
di.buildFromEdges(edges);
const int64_t total_arcs = static_cast<int64_t>(edges.size());

std::vector<double> supply(n_nodes);
for (int64_t i = 0; i < n_nodes; ++i) {
supply[i] = X[i] - Y[i];
}

typedef NetworkSimplexSimple<Digraph, double, double, node_id_type> Simplex;
Simplex::SimplexOptions simplex_options(true);
Simplex net(di, simplex_options, static_cast<int>(n_nodes), total_arcs, maxIter);
net.supplyMap(supply);
for (int64_t k = 0; k < total_arcs; ++k) {
net.setCost(Digraph::arcFromId(k), 1.0);
}

int ret = net.run();
if (ret != (int)net.OPTIMAL && ret != (int)net.MAX_ITER_REACHED) {
return ret;
}

*cost = net.totalCost();

// Node potentials (dual variables) are a byproduct of the solve, cheap
// to extract regardless of whether a plan was requested: dW/dX[i] =
// alpha[i], dW/dY[i] = -alpha[i] (beta = -alpha, since supply[i] =
// X[i] - Y[i] uses a single graph, not a bipartite source/target split).
// Negated to match LEMON's sign convention, same as the bipartite
// extract_compressed_support above (alpha = -potential).
for (int64_t i = 0; i < n_nodes; ++i) {
alpha[i] = -net.potential(Digraph::nodeFromId(static_cast<int>(i)));
}

if (!return_plan) {
// The caller only wants the cost: skip decomposing the Beckmann-style
// arc flow into a transportation plan (coupling) entirely.
return ret;
}

// A bin's mass that already overlaps between X and Y needs no transport,
// so the min-cost flow above never routes it and the arc-flow
// decomposition below never reports it. Emit it directly as a same-bin
// plan entry so the plan is a genuine coupling (row sums X, column sums
// Y), not just the net residual.
for (int64_t i = 0; i < n_nodes; ++i) {
const double self_mass = std::min(X[i], Y[i]);
if (self_mass > 1e-10) {
if (*n_plan_entries_out >= max_plan_entries) {
return (int)net.MAX_ITER_REACHED;
}
plan_sources_out[*n_plan_entries_out] = static_cast<uint64_t>(i);
plan_targets_out[*n_plan_entries_out] = static_cast<uint64_t>(i);
plan_values_out[*n_plan_entries_out] = self_mass;
++(*n_plan_entries_out);
}
}

// Decompose the arc flow into a direct (source_bin, target_bin, mass)
// transportation plan.
std::vector<std::vector<GridFlowEdge>> flow_adj(n_nodes);
for (int64_t k = 0; k < total_arcs; ++k) {
const Digraph::Arc a = Digraph::arcFromId(k);
const double f = net.flow(a);
if (f > 1e-10) {
flow_adj[di.source(a)].push_back({di.target(a), f});
}
}

if (!decompose_grid_flows(flow_adj, supply, plan_sources_out, plan_targets_out,
plan_values_out, n_plan_entries_out, max_plan_entries)) {
return (int)net.MAX_ITER_REACHED;
}

return ret;
}

int EMD_wrap_lazy(int n1, int n2, double *X, double *Y, double *coords_a, double *coords_b,
int dim, int metric, uint64_t *flow_sources_out,
uint64_t *flow_targets_out, double *flow_values_out,
uint64_t *n_flows_out, uint64_t max_flows_out,
Expand Down
Loading
Loading