# Source code for ot.regpath

# -*- coding: utf-8 -*-
"""
Regularization path OT solvers
"""

# Author: Haoran Wu <haoran.wu@univ-ubs.fr>

import numpy as np
import scipy.sparse as sp

[docs]def recast_ot_as_lasso(a, b, C):
r"""This function recasts the l2-penalized UOT problem as a Lasso problem.

Recall the l2-penalized UOT problem defined in
:ref: <references-regpath>

.. math::
\text{UOT}_{\lambda} = \min_T <C, T> + \lambda \|T 1_m -
\mathbf{a}\|_2^2 +
\lambda \|T^T 1_n - \mathbf{b}\|_2^2

s.t.
T \geq 0

where :

- :math:C is the cost matrix
- :math:\lambda is the l2-regularization parameter
- :math:\mathbf{a} and :math:\mathbf{b} are the source and target \
distributions
- :math:T is the transport plan to optimize

The problem above can be reformulated as a non-negative penalized
linear regression problem, particularly Lasso

.. math::
\text{UOT2}_{\lambda} = \min_{\mathbf{t}} \gamma \mathbf{c}^T
\mathbf{t} + 0.5 * \|H \mathbf{t} - \mathbf{y}\|_2^2

s.t.
\mathbf{t} \geq 0

where :

- :math:\mathbf{c} is the flattened version of the cost matrix :math:C
- :math:\mathbf{y} is the concatenation of vectors :math:\mathbf{a} \
and :math:\mathbf{b}
- :math:H is a  metric matrix, see :ref: <references-regpath> for \
the design of :math:H. The matrix product :math:H\mathbf{t} \
computes both the source marginal and the target marginals.
- :math:\mathbf{t} is the flattened version of the transport plan \
:math:T

Parameters
----------
a : np.ndarray (dim_a,)
Histogram of dimension dim_a
b : np.ndarray (dim_b,)
Histogram of dimension dim_b
C : np.ndarray, shape (dim_a, dim_b)
Cost matrix

Returns
-------
H : np.ndarray (dim_a+dim_b, dim_a*dim_b)
Design matrix that contains only 0 and 1
y : np.ndarray (ns + nt, )
Concatenation of histograms :math:\mathbf{a} and :math:\mathbf{b}
c : np.ndarray (ns * nt, )
Flattened array of the cost matrix

Examples
--------
>>> import ot
>>> a = np.array([0.2, 0.3, 0.5])
>>> b = np.array([0.1, 0.9])
>>> C = np.array([[16., 25.], [28., 16.], [40., 36.]])
>>> H, y, c = ot.regpath.recast_ot_as_lasso(a, b, C)
>>> H.toarray()
array([[1., 1., 0., 0., 0., 0.],
[0., 0., 1., 1., 0., 0.],
[0., 0., 0., 0., 1., 1.],
[1., 0., 1., 0., 1., 0.],
[0., 1., 0., 1., 0., 1.]])
>>> y
array([0.2, 0.3, 0.5, 0.1, 0.9])
>>> c
array([16., 25., 28., 16., 40., 36.])

References
----------
..  Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021).
Unbalanced optimal transport through non-negative penalized
linear regression. NeurIPS.
"""

dim_a = np.shape(a)
dim_b = np.shape(b)
y = np.concatenate((a, b))
c = C.flatten()
jHa = np.arange(dim_a * dim_b)
iHa = np.repeat(np.arange(dim_a), dim_b)
jHb = np.arange(dim_a * dim_b)
iHb = np.tile(np.arange(dim_b), dim_a) + dim_a
j = np.concatenate((jHa, jHb))
i = np.concatenate((iHa, iHb))
H = sp.csc_matrix((np.ones(dim_a * dim_b * 2), (i, j)),
shape=(dim_a + dim_b, dim_a * dim_b))
return H, y, c

[docs]def recast_semi_relaxed_as_lasso(a, b, C):
r"""This function recasts the semi-relaxed l2-UOT problem as Lasso problem.

.. math::

\text{semi-relaxed UOT} = \min_T <C, T>
+ \lambda \|T 1_m - \mathbf{a}\|_2^2

s.t.
T^T 1_n = \mathbf{b}

\mathbf{t} \geq 0

where :

- :math:C is the metric cost matrix
- :math:\lambda is the l2-regularization parameter
- :math:\mathbf{a} and :math:\mathbf{b} are the source and target \
distributions
- :math:T is the transport plan to optimize

The problem above can be reformulated as follows

.. math::
\text{semi-relaxed UOT2} = \min_t \gamma \mathbf{c}^T t
+ 0.5 * \|H_r \mathbf{t} - \mathbf{a}\|_2^2

s.t.
H_c \mathbf{t} = \mathbf{b}

\mathbf{t} \geq 0

where :

- :math:\mathbf{c} is flattened version of the cost matrix :math:C
- :math:\gamma = 1/\lambda is the l2-regularization parameter
- :math:H_r is  a  metric matrix which computes the sum along the \
rows of the transport plan :math:T
- :math:H_c is a  metric matrix which computes the sum along the \
columns of the transport plan :math:T
- :math:\mathbf{t} is the flattened version of :math:T

Parameters
----------
a : np.ndarray (dim_a,)
Histogram of dimension dim_a
b : np.ndarray (dim_b,)
Histogram of dimension dim_b
C : np.ndarray, shape (dim_a, dim_b)
Cost matrix

Returns
-------
Hr : np.ndarray (dim_a, dim_a * dim_b)
Auxiliary matrix constituted by 0 and 1, which computes
the sum along the rows of transport plan :math:T
Hc : np.ndarray (dim_b, dim_a * dim_b)
Auxiliary matrix constituted by 0 and 1, which computes
the sum along the columns of transport plan :math:T
c : np.ndarray (ns * nt, )
Flattened array of the cost matrix

Examples
--------
>>> import ot
>>> a = np.array([0.2, 0.3, 0.5])
>>> b = np.array([0.1, 0.9])
>>> C = np.array([[16., 25.], [28., 16.], [40., 36.]])
>>> Hr,Hc,c = ot.regpath.recast_semi_relaxed_as_lasso(a, b, C)
>>> Hr.toarray()
array([[1., 1., 0., 0., 0., 0.],
[0., 0., 1., 1., 0., 0.],
[0., 0., 0., 0., 1., 1.]])
>>> Hc.toarray()
array([[1., 0., 1., 0., 1., 0.],
[0., 1., 0., 1., 0., 1.]])
>>> c
array([16., 25., 28., 16., 40., 36.])
"""

dim_a = np.shape(a)
dim_b = np.shape(b)

c = C.flatten()
jHr = np.arange(dim_a * dim_b)
iHr = np.repeat(np.arange(dim_a), dim_b)
jHc = np.arange(dim_a * dim_b)
iHc = np.tile(np.arange(dim_b), dim_a)

Hr = sp.csc_matrix((np.ones(dim_a * dim_b), (iHr, jHr)),
shape=(dim_a, dim_a * dim_b))
Hc = sp.csc_matrix((np.ones(dim_a * dim_b), (iHc, jHc)),
shape=(dim_b, dim_a * dim_b))

return Hr, Hc, c

[docs]def ot_next_gamma(phi, delta, HtH, Hty, c, active_index, current_gamma):
r""" This function computes the next value of gamma if a variable
is added in the next iteration of the regularization path.

We look for the largest value of gamma such that
the gradient of an inactive variable vanishes

.. math::
\max_{i \in \bar{A}} \frac{\mathbf{h}_i^T(H_A \phi - \mathbf{y})}
{\mathbf{h}_i^T H_A \delta - \mathbf{c}_i}

where :

- A is the current active set
- :math:\mathbf{h}_i is the :math:i th column of the design \
matrix :math:{H}
- :math:{H}_A is the sub-matrix constructed by the columns of \
:math:{H} whose indices belong to the active set A
- :math:\mathbf{c}_i is the :math:i th element of the cost vector \
:math:\mathbf{c}
- :math:\mathbf{y} is the concatenation of the source and target \
distributions
- :math:\phi is the intercept of the solutions at the current iteration
- :math:\delta is the slope of the solutions at the current iteration

Parameters
----------
phi : np.ndarray (size(A), )
Intercept of the solutions at the current iteration
delta : np.ndarray (size(A), )
Slope of the solutions at the current iteration
HtH : np.ndarray (dim_a * dim_b, dim_a * dim_b)
Matrix product of :math:{H}^T {H}
Hty : np.ndarray (dim_a + dim_b, )
Matrix product of :math:{H}^T \mathbf{y}
c: np.ndarray (dim_a * dim_b, )
Flattened array of the cost matrix :math:{C}
active_index : list
Indices of active variables
current_gamma : float
Value of the regularization parameter at the beginning of the current \
iteration

Returns
-------
next_gamma : float
Value of gamma if a variable is added to active set in next iteration
next_active_index : int
Index of variable to be activated

References
----------
..  Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021).
Unbalanced optimal transport through non-negative penalized
linear regression. NeurIPS.
"""
M = (HtH[:, active_index].dot(phi) - Hty) / \
(HtH[:, active_index].dot(delta) - c + 1e-16)
M[active_index] = 0
M[M > (current_gamma - 1e-10 * current_gamma)] = 0
return np.max(M), np.argmax(M)

[docs]def semi_relaxed_next_gamma(phi, delta, phi_u, delta_u, HrHr, Hc, Hra,
c, active_index, current_gamma):
r""" This function computes the next value of gamma when a variable is
active in the regularization path of semi-relaxed UOT.

By taking the Lagrangian form of the problem, we obtain a similar update
as the two-sided relaxed UOT

.. math::

\max_{i \in \bar{A}} \frac{\mathbf{h}_{ri}^T(H_{rA} \phi - \mathbf{a})
+ \mathbf{h}_{c i}^T\phi_u}{\mathbf{h}_{r i}^T H_{r A} \delta + \
\mathbf{h}_{c i} \delta_u - \mathbf{c}_i}

where :

- A is the current active set
- :math:\mathbf{h}_{r i} is the ith column of the matrix :math:H_r
- :math:\mathbf{h}_{c i} is the ith column of the matrix :math:H_c
- :math:H_{r A} is the sub-matrix constructed by the columns of \
:math:H_r whose indices belong to the active set A
- :math:\mathbf{c}_i is the :math:i th element of cost vector \
:math:\mathbf{c}
- :math:\phi is the intercept of the solutions in current iteration
- :math:\delta is the slope of the solutions in current iteration
- :math:\phi_u is the intercept of Lagrange parameter at the \
current iteration
- :math:\delta_u is the slope of Lagrange parameter at the \
current iteration

Parameters
----------
phi : np.ndarray (size(A), )
Intercept of the solutions at the current iteration
delta : np.ndarray (size(A), )
Slope of the solutions at the current iteration
phi_u : np.ndarray (dim_b, )
Intercept of the Lagrange parameter at the current iteration
delta_u : np.ndarray (dim_b, )
Slope of the Lagrange parameter at the current iteration
HrHr : np.ndarray (dim_a * dim_b, dim_a * dim_b)
Matrix product of :math:H_r^T H_r
Hc : np.ndarray (dim_b, dim_a * dim_b)
Matrix that computes the sum along the columns of the transport plan \
:math:T
Hra : np.ndarray (dim_a * dim_b, )
Matrix product of :math:H_r^T \mathbf{a}
c: np.ndarray (dim_a * dim_b, )
Flattened array of cost matrix :math:C
active_index : list
Indices of active variables
current_gamma : float
Value of regularization coefficient at the start of current iteration

Returns
-------
next_gamma : float
Value of gamma if a variable is added to active set in next iteration
next_active_index : int
Index of variable to be activated

References
----------
..  Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021).
Unbalanced optimal transport through non-negative penalized
linear regression. NeurIPS.
"""

M = (HrHr[:, active_index].dot(phi) - Hra + Hc.T.dot(phi_u)) / \
(HrHr[:, active_index].dot(delta) - c + Hc.T.dot(delta_u) + 1e-16)
M[active_index] = 0
M[M > (current_gamma - 1e-10 * current_gamma)] = 0
return np.max(M), np.argmax(M)

[docs]def compute_next_removal(phi, delta, current_gamma):
r""" This function computes the next gamma value if a variable
is removed at the next iteration of the regularization path.

We look for the largest value of the regularization parameter such that
an element of the current solution vanishes

.. math::
\max_{j \in A} \frac{\phi_j}{\delta_j}

where :

- A is the current active set
- :math:\phi_j is the :math:j th element of the intercept of the \
current solution
- :math:\delta_j is the :math:j th element of the slope of the \
current solution

Parameters
----------
phi : ndarray, shape (size(A), )
Intercept of the solution at the current iteration
delta : ndarray, shape (size(A), )
Slope of the solution at the current iteration
current_gamma : float
Value of the regularization parameter at the beginning of the \
current iteration

Returns
-------
next_removal_gamma : float
Gamma value if a variable is removed at the next iteration
next_removal_index : int
Index of the variable to be removed at the next iteration

.. _references-regpath:
References
----------
..  Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021).
Unbalanced optimal transport through non-negative penalized
linear regression. NeurIPS.
"""
r_candidate = phi / (delta - 1e-16)
r_candidate[r_candidate >= (1 - 1e-8) * current_gamma] = 0
return np.max(r_candidate), np.argmax(r_candidate)

[docs]def complement_schur(M_current, b, d, id_pop):
r""" This function computes the inverse of the design matrix in the \
regularization path using the  Schur complement. Two cases may arise:

Case 1: one variable is added to the active set

.. math::
M_{k+1}^{-1} =
\begin{bmatrix}
M_{k}^{-1} + s^{-1} M_{k}^{-1} \mathbf{b} \mathbf{b}^T M_{k}^{-1} \
& - M_{k}^{-1} \mathbf{b} s^{-1}   \\
- s^{-1} \mathbf{b}^T M_{k}^{-1} & s^{-1}
\end{bmatrix}

where :

- :math:M_k^{-1} is the inverse of the design matrix :math:H_A^tH_A \
of the previous iteration
- :math:\mathbf{b} is the last column of :math:M_{k}
- :math:s is the Schur complement, given by  \
:math:s = \mathbf{d} - \mathbf{b}^T M_{k}^{-1} \mathbf{b}

Case 2: one variable is removed from the active set.

.. math::
M_{k+1}^{-1} = M^{-1}_{k \backslash q} -
\frac{r_{-q,q} r^{T}_{-q,q}}{r_{q,q}}

where :

- :math:q is the index of column and row to delete
- :math:M^{-1}_{k \backslash q} is the previous inverse matrix deprived \
of the :math:q th column and :math:q th row
- :math:r_{-q,q} is the :math:q th column of :math:M^{-1}_{k} \
without the :math:q th element
- :math:r_{q, q} is the element of :math:q th column and :math:q th \
row in :math:M^{-1}_{k}

Parameters
----------
M_current : ndarray, shape (size(A)-1, size(A)-1)
Inverse matrix of :math:H_A^tH_A at the previous iteration, with \
size(A) the size of the active set
b : ndarray, shape (size(A)-1, )
None for case 2 (removal), last column of :math:M_{k} for case 1 \
d : float
should be equal to 2 when UOT and 1 for the semi-relaxed OT
id_pop : int
Index of the variable to be removed,  equal to -1
if no variable is deleted at the current iteration

Returns
-------
M : ndarray, shape (size(A), size(A))
Inverse matrix of :math:H_A^tH_A of the current iteration

References
----------
..  Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021).
Unbalanced optimal transport through non-negative penalized
linear regression. NeurIPS.
"""

if b is None:
b = M_current[id_pop, :]
b = np.delete(b, id_pop)
M_del = np.delete(M_current, id_pop, 0)
a = M_del[:, id_pop]
M_del = np.delete(M_del, id_pop, 1)
M = M_del - np.outer(a, b) / M_current[id_pop, id_pop]
else:
n = b.shape + 1
if np.shape(b) == 0:
M = np.array([[0.5]])
else:
X = M_current.dot(b)
s = d - b.T.dot(X)
M = np.zeros((n, n))
M[:-1, :-1] = M_current + X.dot(X.T) / s
X_ravel = X.ravel()
M[-1, :-1] = -X_ravel / s
M[:-1, -1] = -X_ravel / s
M[-1, -1] = 1 / s
return M

[docs]def construct_augmented_H(active_index, m, Hc, HrHr):
r""" This function constructs an augmented matrix for the first iteration
of the semi-relaxed regularization path

.. math::
\text{Augmented}_H =
\begin{bmatrix}
0 & H_{c A} \\
H_{c A}^T & H_{r A}^T H_{r A}
\end{bmatrix}

where :

- :math:H_{r A} is the sub-matrix constructed by the columns of \
:math:H_r whose indices belong to the active set A
- :math:H_{c A} is the sub-matrix constructed by the columns of \
:math:H_c whose indices belong to the active set A

Parameters
----------
active_index : list
Indices of the active variables
m : int
Length of the target distribution
Hc : np.ndarray (dim_b, dim_a * dim_b)
Matrix that computes the sum along the columns of the transport plan \
:math:T
HrHr : np.ndarray (dim_a * dim_b, dim_a * dim_b)
Matrix product of :math:H_r^T H_r

Returns
-------
H_augmented : np.ndarray (dim_b + size(A), dim_b + size(A))
Augmented matrix for the first iteration of the semi-relaxed
regularization path
"""
Hc_sub = Hc[:, active_index].toarray()
HrHr_sub = HrHr[:, active_index]
HrHr_sub = HrHr_sub[active_index, :].toarray()
H_augmented = np.block([[np.zeros((m, m)), Hc_sub], [Hc_sub.T, HrHr_sub]])
return H_augmented

[docs]def fully_relaxed_path(a: np.array, b: np.array, C: np.array, reg=1e-4,
itmax=50000):
r"""This function gives the regularization path of l2-penalized UOT problem

The problem to optimize is the Lasso reformulation of the l2-penalized UOT:

.. math::
\min_t \gamma \mathbf{c}^T \mathbf{t}
+ 0.5 * \|{H} \mathbf{t} - \mathbf{y}\|_2^2

s.t.
\mathbf{t} \geq 0

where :

- :math:\mathbf{c} is the flattened version of the cost matrix \
:math:{C}
- :math:\gamma = 1/\lambda is the l2-regularization coefficient
- :math:\mathbf{y} is the concatenation of vectors :math:\mathbf{a} \
and :math:\mathbf{b}, defined as \
:math:\mathbf{y}^T = [\mathbf{a}^T \mathbf{b}^T]
- :math:{H} is a design matrix, see :ref: <references-regpath> \
for the design of :math:{H}. The matrix product :math:H\mathbf{t} \
computes both the source marginal and the target marginals.
- :math:\mathbf{t} is the flattened version of the transport matrix

Parameters
----------
a : np.ndarray (dim_a,)
Histogram of dimension dim_a
b : np.ndarray (dim_b,)
Histogram of dimension dim_b
C : np.ndarray, shape (dim_a, dim_b)
Cost matrix
reg: float
l2-regularization coefficient
itmax: int
Maximum number of iteration
Returns
-------
t : np.ndarray (dim_a*dim_b, )
Flattened vector of the optimal transport matrix
t_list : list
List of solutions in the regularization path
gamma_list : list
List of regularization coefficients in the regularization path

Examples
--------
>>> import ot
>>> import numpy as np
>>> n = 3
>>> xs = np.array([1., 2., 3.]).reshape((n, 1))
>>> xt = np.array([5., 6., 7.]).reshape((n, 1))
>>> C = ot.dist(xs, xt)
>>> C /= C.max()
>>> a = np.array([0.2, 0.5, 0.3])
>>> b = np.array([0.2, 0.5, 0.3])
>>> t, _, _ = ot.regpath.fully_relaxed_path(a, b, C, 1e-4)
>>> t
array([1.99958333e-01, 0.00000000e+00, 0.00000000e+00, 3.88888889e-05,
4.99938889e-01, 0.00000000e+00, 0.00000000e+00, 3.88888889e-05,
2.99958333e-01])

References
----------
..  Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021).
Unbalanced optimal transport through non-negative penalized
linear regression. NeurIPS.
"""

n = np.shape(a)
m = np.shape(b)
H, y, c = recast_ot_as_lasso(a, b, C)
HtH = H.T.dot(H)
Hty = H.T.dot(y)
n_iter = 1

# initialization
M0 = Hty / c
gamma_list = [np.max(M0)]
active_index = [np.argmax(M0)]
t_list = [np.zeros((n * m,))]
H_inv = np.array([[]])
id_pop = -1

while n_iter < itmax and gamma_list[-1] > reg:
H_inv = complement_schur(H_inv, add_col, 2., id_pop)
current_gamma = gamma_list[-1]

# compute the intercept and slope of solutions in current iteration
# t = phi - gamma * delta
phi = H_inv.dot(Hty[active_index])
delta = H_inv.dot(c[active_index])
gamma, ik = ot_next_gamma(phi, delta, HtH, Hty, c, active_index,
current_gamma)

# compute the next lambda when removing a point from the active set
alt_gamma, id_pop = compute_next_removal(phi, delta, current_gamma)

# if the positivity constraint is violated, we remove id_pop
# from active set, otherwise we add ik to active set
if alt_gamma > gamma:
gamma = alt_gamma
else:
id_pop = -1

# compute the solution of current segment
tA = phi - gamma * delta
sol = np.zeros((n * m, ))
sol[active_index] = tA

if id_pop != -1:
active_index.pop(id_pop)
else:
active_index.append(ik)

gamma_list.append(gamma)
t_list.append(sol)
n_iter += 1

if itmax <= n_iter:
print('maximum iteration has been reached !')

# correct the last solution and gamma
if len(t_list) > 1:
t_final = (t_list[-2] + (t_list[-1] - t_list[-2]) *
(reg - gamma_list[-2]) / (gamma_list[-1] - gamma_list[-2]))
t_list[-1] = t_final
gamma_list[-1] = reg
else:
gamma_list[-1] = reg
print('Regularization path does not exist !')

return t_list[-1], t_list, gamma_list

[docs]def semi_relaxed_path(a: np.array, b: np.array, C: np.array, reg=1e-4,
itmax=50000):
r"""This function gives the regularization path of semi-relaxed
l2-UOT problem.

The problem to optimize is the Lasso reformulation of the l2-penalized UOT:

.. math::

\min_t \gamma \mathbf{c}^T t
+ 0.5 * \|H_r \mathbf{t} - \mathbf{a}\|_2^2

s.t.
H_c \mathbf{t} = \mathbf{b}

\mathbf{t} \geq 0

where :

- :math:\mathbf{c} is the flattened version of the cost matrix \
:math:C
- :math:\gamma = 1/\lambda is the l2-regularization parameter
- :math:H_r is  a  matrix that computes the sum along the rows of \
the transport plan :math:T
- :math:H_c is  a  matrix that computes the sum along the columns of \
the transport plan :math:T
- :math:\mathbf{t} is the flattened version of the transport plan \
:math:T

Parameters
----------
a : np.ndarray (dim_a,)
Histogram of dimension dim_a
b : np.ndarray (dim_b,)
Histogram of dimension dim_b
C : np.ndarray, shape (dim_a, dim_b)
Cost matrix
reg: float (optional)
l2-regularization coefficient
itmax: int (optional)
Maximum number of iteration

Returns
-------
t : np.ndarray (dim_a*dim_b, )
Flattened vector of the (unregularized) optimal transport matrix
t_list : list
List of all the optimal transport vectors of the regularization path
gamma_list : list
List of the regularization parameters in the path

Examples
--------
>>> import ot
>>> import numpy as np
>>> n = 3
>>> xs = np.array([1., 2., 3.]).reshape((n, 1))
>>> xt = np.array([5., 6., 7.]).reshape((n, 1))
>>> C = ot.dist(xs, xt)
>>> C /= C.max()
>>> a = np.array([0.2, 0.5, 0.3])
>>> b = np.array([0.2, 0.5, 0.3])
>>> t, _, _ = ot.regpath.semi_relaxed_path(a, b, C, 1e-4)
>>> t
array([1.99980556e-01, 0.00000000e+00, 0.00000000e+00, 1.94444444e-05,
4.99980556e-01, 0.00000000e+00, 0.00000000e+00, 1.94444444e-05,
3.00000000e-01])

References
----------
..  Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021).
Unbalanced optimal transport through non-negative penalized
linear regression. NeurIPS.
"""

n = np.shape(a)
m = np.shape(b)
Hr, Hc, c = recast_semi_relaxed_as_lasso(a, b, C)
Hra = Hr.T.dot(a)
HrHr = Hr.T.dot(Hr)
n_iter = 1
active_index = []

# initialization
for j in range(np.shape(C)):
i = np.argmin(C[:, j])
active_index.append(i * m + j)
gamma_list = []
t_list = []
current_gamma = np.Inf
augmented_H0 = construct_augmented_H(active_index, m, Hc, HrHr)
id_pop = -1

while n_iter < itmax and current_gamma > reg:
if n_iter == 1:
H_inv = np.linalg.inv(augmented_H0)
else:
H_inv = complement_schur(H_inv, add_col, 1., id_pop + m)
# compute the intercept and slope of solutions in current iteration
augmented_phi = H_inv.dot(np.concatenate((b, Hra[active_index])))
augmented_delta = H_inv[:, m:].dot(c[active_index])
phi = augmented_phi[m:]
delta = augmented_delta[m:]
phi_u = augmented_phi[0:m]
delta_u = augmented_delta[0:m]
gamma, ik = semi_relaxed_next_gamma(phi, delta, phi_u, delta_u,
HrHr, Hc, Hra, c, active_index,
current_gamma)

# compute the next lambda when removing a point from the active set
alt_gamma, id_pop = compute_next_removal(phi, delta, current_gamma)

# if the positivity constraint is violated, we remove id_pop
# from active set, otherwise we add ik to active set
if alt_gamma > gamma:
gamma = alt_gamma
else:
id_pop = -1

# compute the solution of current segment
tA = phi - gamma * delta
sol = np.zeros((n * m, ))
sol[active_index] = tA
if id_pop != -1:
active_index.pop(id_pop)
else:
active_index.append(ik)
HrHr.toarray()[active_index[:-1], ik]))

gamma_list.append(gamma)
t_list.append(sol)
current_gamma = gamma
n_iter += 1

if itmax <= n_iter:
print('maximum iteration has been reached !')

# correct the last solution and gamma
if len(t_list) > 1:
t_final = (t_list[-2] + (t_list[-1] - t_list[-2]) *
(reg - gamma_list[-2]) / (gamma_list[-1] - gamma_list[-2]))
t_list[-1] = t_final
gamma_list[-1] = reg
else:
gamma_list[-1] = reg
print('Regularization path does not exist !')

return t_list[-1], t_list, gamma_list

[docs]def regularization_path(a: np.array, b: np.array, C: np.array, reg=1e-4,
semi_relaxed=False, itmax=50000):
r"""This function provides all the solutions of the regularization path \
of the l2-UOT problem :ref: <references-regpath>.

The problem to optimize is the Lasso reformulation of the l2-penalized UOT:

.. math::
\min_t \gamma \mathbf{c}^T \mathbf{t}
+ 0.5 * \|{H} \mathbf{t} - \mathbf{y}\|_2^2

s.t.
\mathbf{t} \geq 0

where :

- :math:\mathbf{c} is the flattened version of the cost matrix \
:math:{C}
- :math:\gamma = 1/\lambda is the l2-regularization coefficient
- :math:\mathbf{y} is the concatenation of vectors :math:\mathbf{a} \
and :math:\mathbf{b}, defined as \
:math:\mathbf{y}^T = [\mathbf{a}^T \mathbf{b}^T]
- :math:{H} is a design matrix, see :ref: <references-regpath> \
for the design of :math:{H}. The matrix product :math:H\mathbf{t} \
computes both the source marginal and the target marginals.
- :math:\mathbf{t} is the flattened version of the transport matrix

For the semi-relaxed problem, it optimizes the Lasso reformulation of the
l2-penalized UOT:

.. math::

\min_t \gamma \mathbf{c}^T \mathbf{t}
+ 0.5 * \|H_r \mathbf{t} - \mathbf{a}\|_2^2

s.t.
H_c \mathbf{t} = \mathbf{b}

\mathbf{t} \geq 0

Parameters
----------
a : np.ndarray (dim_a,)
Histogram of dimension dim_a
b : np.ndarray (dim_b,)
Histogram of dimension dim_b
C : np.ndarray, shape (dim_a, dim_b)
Cost matrix
reg: float (optional)
l2-regularization coefficient
semi_relaxed : bool (optional)
Give the semi-relaxed path if True
itmax: int (optional)
Maximum number of iteration

Returns
-------
t : np.ndarray (dim_a*dim_b, )
Flattened vector of the (unregularized) optimal transport matrix
t_list : list
List of all the optimal transport vectors of the regularization path
gamma_list : list
List of the regularization parameters in the path

References
----------
..  Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021).
Unbalanced optimal transport through non-negative penalized
linear regression. NeurIPS.
"""
if semi_relaxed:
t, t_list, gamma_list = semi_relaxed_path(a, b, C, reg=reg,
itmax=itmax)
else:
t, t_list, gamma_list = fully_relaxed_path(a, b, C, reg=reg,
itmax=itmax)
return t, t_list, gamma_list

[docs]def compute_transport_plan(gamma, gamma_list, Pi_list):
r""" Given the regularization path, this function computes the transport
plan for any value of gamma thanks to the piecewise linearity of the path.

.. math::
t(\gamma) = \phi(\gamma) - \gamma \delta(\gamma)

where:

- :math:\gamma is the regularization parameter
- :math:\phi(\gamma) is the corresponding intercept
- :math:\delta(\gamma) is the corresponding slope
- :math:\mathbf{t} is the flattened version of the transport matrix

Parameters
----------
gamma : float
Regularization coefficient
gamma_list : list
List of regularization parameters of the regularization path
Pi_list : list
List of all the solutions of the regularization path

Returns
-------
t : np.ndarray (dim_a*dim_b, )
Vectorization of the transport plan corresponding to the given value
of gamma

Examples
--------
>>> import ot
>>> import numpy as np
>>> n = 3
>>> xs = np.array([1., 2., 3.]).reshape((n, 1))
>>> xt = np.array([5., 6., 7.]).reshape((n, 1))
>>> C = ot.dist(xs, xt)
>>> C /= C.max()
>>> a = np.array([0.2, 0.5, 0.3])
>>> b = np.array([0.2, 0.5, 0.3])
>>> t, pi_list, g_list = ot.regpath.regularization_path(a, b, C, reg=1e-4)
>>> gamma = 1
>>> t2 = ot.regpath.compute_transport_plan(gamma, g_list, pi_list)
>>> t2
array([0.        , 0.        , 0.        , 0.19722222, 0.05555556,
0.        , 0.        , 0.24722222, 0.        ])

.. _references-regpath:
References
----------
..  Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021).
Unbalanced optimal transport through non-negative penalized
linear regression. NeurIPS.
"""

if gamma >= gamma_list:
Pi = Pi_list
elif gamma <= gamma_list[-1]:
Pi = Pi_list[-1]
else:
idx = np.where(gamma <= np.array(gamma_list))[-1]
gamma_k0 = gamma_list[idx]
gamma_k1 = gamma_list[idx + 1]
pi_k0 = Pi_list[idx]
pi_k1 = Pi_list[idx + 1]
Pi = pi_k0 + (pi_k1 - pi_k0) * (gamma - gamma_k0) \
/ (gamma_k1 - gamma_k0)
return Pi