Skip to content

Commit 0da9fc7

Browse files
Address review on semi-discrete OT module:
- Rename public functions to semidiscrete_{atom_weights,ot_map,c_transform} and align params with ot.solve_sample (X_target, sampler_source, a_target, metric, max_iter, max_cost) - metric accepts ot.dist strings (default 'sqeuclidean') or a callable - sampler_source accepts built-in strings ('unif', 'ball', 'normal', ...), default 'unif' - Full docstrings with equations and references to both papers - Examplel OT-map visualization with arrow and cell-mass plot - Tests: use nx.from_numpy, cover the string metric/sampler paths
1 parent eba3ba3 commit 0da9fc7

3 files changed

Lines changed: 500 additions & 168 deletions

File tree

examples/others/plot_semidiscrete.py

Lines changed: 93 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
:math:`\langle g, b\rangle + \mathbb{E}_X[\varphi_g(X)]` estimated by
2323
Monte Carlo, where the c-transform
2424
:math:`\varphi_g(x) = \min_j\big(c(x, y_j) - g_j\big)` is computed by
25-
:func:`ot.semidiscrete.c_transform`. The solver **maximises** this
25+
:func:`ot.semidiscrete.semidiscrete_c_transform`. The solver **maximises** this
2626
objective.
2727
2828
.. [90] Genans, F., Godichon-Baggioni, A., Vialard, F.-X., Wintenberger, O.
@@ -41,8 +41,9 @@
4141

4242
from ot.semidiscrete import (
4343
solve_semidiscrete,
44-
atom_weights,
45-
c_transform,
44+
semidiscrete_atom_weights,
45+
semidiscrete_c_transform,
46+
semidiscrete_ot_map,
4647
)
4748

4849
##############################################################################
@@ -60,20 +61,20 @@ def source_sampler(batch_size):
6061
target_positions = 0.1 + 0.8 * np.random.default_rng(0).random((n_atoms, 2))
6162

6263

63-
def plot_laguerre_cells(target, g, ax, title, resolution=300):
64+
def plot_laguerre_cells(target, g, ax, title, resolution=300, alpha=0.55):
6465
xs = np.linspace(0, 1, resolution)
6566
ys = np.linspace(0, 1, resolution)
6667
XX, YY = np.meshgrid(xs, ys)
6768
grid = np.stack([XX.ravel(), YY.ravel()], axis=1)
68-
labels = atom_weights(target, grid, g, reg=0.0).argmax(axis=1)
69+
labels = semidiscrete_atom_weights(target, grid, g, reg=0.0).argmax(axis=1)
6970
image = labels.reshape(resolution, resolution)
7071
cmap = plt.get_cmap("tab20", target.shape[0])
7172
ax.imshow(
7273
image,
7374
origin="lower",
7475
extent=(0, 1, 0, 1),
7576
cmap=cmap,
76-
alpha=0.55,
77+
alpha=alpha,
7778
vmin=-0.5,
7879
vmax=target.shape[0] - 0.5,
7980
interpolation="nearest",
@@ -101,21 +102,61 @@ def plot_laguerre_cells(target, g, ax, title, resolution=300):
101102
# A single call to :func:`solve_semidiscrete` runs DRAG with the default
102103
# arguments (``decreasing_reg=True``). We show the initial Voronoi cells
103104
# (:math:`g = 0`) next to the Laguerre cells at the optimum.
104-
# In this problem, the maximum cost between samples is 1.0, so we pass it as
105-
# ``max_cost=1.0``. Knowing this bound, the potential values are clipped to
106-
# [-max_cost, max_cost], where it is known that an optimal potential lies ([90]_, Lemma 1),
107-
# which speeds up convergence.
105+
# With the squared-Euclidean cost (default ``metric='sqeuclidean'``), the cost
106+
# between a source point in :math:`[0, 1]^2` and an atom is
107+
# :math:`\|x - y\|^2 \le 2`. We clip the potential to
108+
# ``[-max_cost, max_cost] = [-2, 2]``, the localizing set where an optimal
109+
# potential lies ([90]_, Lemma 1), which speeds up convergence.
108110
g_drag = solve_semidiscrete(
109111
target_positions,
110112
source_sampler,
111-
n_iter=20_000,
112-
batch_size=16,
113-
max_cost=1.0,
113+
max_iter=20_000,
114+
batch_size=32,
115+
max_cost=2.0,
114116
)
115117

116118
fig, axes = plt.subplots(1, 2, figsize=(11, 5.5))
117119
plot_laguerre_cells(target_positions, np.zeros(n_atoms), axes[0], "Voronoi (g = 0)")
118-
plot_laguerre_cells(target_positions, g_drag, axes[1], "DRAG")
120+
plot_laguerre_cells(target_positions, g_drag, axes[1], "Approximated OT Laguerre cells")
121+
plt.tight_layout()
122+
plt.show()
123+
124+
125+
##############################################################################
126+
# Transport map over the Laguerre cells
127+
# -------------------------------------
128+
#
129+
# :func:`semidiscrete_ot_map` with ``reg=0`` is the hard Monge map: every
130+
# source point is sent to the atom of its Laguerre cell. Overlaying the map
131+
# (arrows on a source grid) on the *faded* cells shows each cell's mass
132+
# collapsing onto its atom -- a direct illustration of the mapping function.
133+
134+
gx = np.linspace(0.04, 0.96, 14)
135+
grid = np.stack([a.ravel() for a in np.meshgrid(gx, gx)], axis=1)
136+
mapped = semidiscrete_ot_map(target_positions, grid, g_drag, reg=0.0)
137+
labels = semidiscrete_atom_weights(target_positions, grid, g_drag, reg=0.0).argmax(
138+
axis=1
139+
)
140+
141+
cmap = plt.get_cmap("tab20", n_atoms)
142+
fig, ax = plt.subplots(figsize=(6.5, 6.5))
143+
plot_laguerre_cells(
144+
target_positions, g_drag, ax, "Approximated OT map over Laguerre cells", alpha=0.22
145+
)
146+
ax.quiver(
147+
grid[:, 0],
148+
grid[:, 1],
149+
mapped[:, 0] - grid[:, 0],
150+
mapped[:, 1] - grid[:, 1],
151+
angles="xy",
152+
scale_units="xy",
153+
scale=1,
154+
width=0.005,
155+
headwidth=4,
156+
headlength=5,
157+
color=[cmap(i) for i in labels],
158+
zorder=2,
159+
)
119160
plt.tight_layout()
120161
plt.show()
121162

@@ -134,15 +175,17 @@ def plot_laguerre_cells(target, g, ax, title, resolution=300):
134175

135176

136177
def cell_masses(target, g, sampler, n_samples=100_000):
137-
labels = atom_weights(target, sampler(n_samples), g, reg=0.0).argmax(axis=1)
178+
labels = semidiscrete_atom_weights(target, sampler(n_samples), g, reg=0.0).argmax(
179+
axis=1
180+
)
138181
counts = np.bincount(labels, minlength=target.shape[0])
139182
return counts / n_samples
140183

141184

142185
def mc_cost(target, g, sampler, n_samples=100_000):
143186
b = np.full(target.shape[0], 1.0 / target.shape[0])
144187
samples = sampler(n_samples)
145-
return float(g @ b + c_transform(target, samples, g, reg=0.0).mean())
188+
return float(g @ b + semidiscrete_c_transform(target, samples, g, reg=0.0).mean())
146189

147190

148191
target_mass = 1.0 / n_atoms
@@ -156,3 +199,37 @@ def mc_cost(target, g, sampler, n_samples=100_000):
156199
f" max: {np.max(np.abs(m_drag - target_mass)):.4f}"
157200
f" semi-dual cost (MC): {cost_drag:.5f}"
158201
)
202+
203+
204+
##############################################################################
205+
# Laguerre-cell masses
206+
# --------------------
207+
#
208+
# At the optimum every cell carries the same mass :math:`1/15`. The bar plot
209+
# shows the empirical mass per cell against this ground truth (dashed line):
210+
# every cell sits close to the theoretical value.
211+
212+
cmap = plt.get_cmap("tab20", n_atoms)
213+
fig, ax = plt.subplots(figsize=(7.5, 4))
214+
ax.bar(
215+
np.arange(n_atoms),
216+
m_drag,
217+
color=[cmap(i) for i in range(n_atoms)],
218+
edgecolor="black",
219+
linewidth=0.6,
220+
)
221+
ax.axhline(
222+
target_mass,
223+
ls="--",
224+
color="black",
225+
lw=1.5,
226+
label="theoretical mass per cell at the optimum",
227+
)
228+
ax.set_ylim(0, 1.6 * target_mass)
229+
ax.set_xticks(np.arange(n_atoms))
230+
ax.set_xlabel("atom index")
231+
ax.set_ylabel("Laguerre-cell mass")
232+
ax.set_title("Approximated OT: Laguerre-cell masses")
233+
ax.legend()
234+
plt.tight_layout()
235+
plt.show()

0 commit comments

Comments
 (0)