Skip to content

Commit 2a2b647

Browse files
authored
Merge pull request #226 from mrava87/featBREAKING-z0
feat[BREAKING]: initial guess of auxiliary variable
2 parents f695b05 + 2748d0c commit 2a2b647

3 files changed

Lines changed: 209 additions & 25 deletions

File tree

‎pyproximal/optimization/primal.py‎

Lines changed: 116 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import numpy as np
77
from pylops.optimization.leastsquares import regularized_inversion
8-
from pylops.utils.backend import to_numpy
8+
from pylops.utils.backend import get_array_module, to_numpy
99
from pylops.utils.typing import NDArray
1010

1111
from pyproximal.proximal import L2
@@ -50,6 +50,58 @@ def ftilde(x: NDArray, y: NDArray, f: ProxOperator, tau: float) -> float:
5050
return z, tau
5151

5252

53+
def _x0z0_init(
54+
x0: NDArray | None,
55+
z0: NDArray | None,
56+
Op: Optional["LinearOperator"] = None,
57+
z0name: Optional[str] = "z0",
58+
Opname: Optional[str] = "Op",
59+
) -> Tuple[NDArray, NDArray]:
60+
r"""Initialize x0 and z0
61+
62+
Initialize x0 and z0 using the following convention.
63+
64+
For ``Op=None``:
65+
- if both are provided, they are simply returned;
66+
- if only one is provided (the other is ``None``), the one provided
67+
is copied to the other one.
68+
69+
For ``Op!=None``, ``x0`` must be provided, and:
70+
- if both are provided, they are simply returned;
71+
- if ``z0`` is not provided, set to ``Op @ x0``.
72+
73+
Parameters
74+
----------
75+
x0 : :obj:`numpy.ndarray`
76+
Initial vector
77+
z0 : :obj:`numpy.ndarray`
78+
Initial auxiliary vector
79+
Op : :obj:`pylops.LinearOperator`, optional
80+
Linear Operator to apply to ``x0``
81+
z0name : :obj:`str`, optional
82+
Name to display in error message instead of ``z0``
83+
Opname : :obj:`str`, optional
84+
Name to display in error message instead of ``Op``
85+
86+
"""
87+
if x0 is None and z0 is None:
88+
raise ValueError(
89+
f"Both x0 or {z0name} are None, provide either of them or both"
90+
)
91+
92+
if Op is None:
93+
if x0 is None:
94+
x0 = z0.copy() # type: ignore[union-attr]
95+
elif z0 is None:
96+
z0 = x0.copy()
97+
else:
98+
if x0 is None:
99+
raise ValueError(f"x0 must be provided when {Opname} is also provided")
100+
elif z0 is None:
101+
z0 = Op @ x0
102+
return x0, z0
103+
104+
53105
def ProximalPoint(
54106
prox: ProxOperator,
55107
x0: NDArray,
@@ -875,15 +927,15 @@ def HQS(
875927
proxg : :obj:`pyproximal.ProxOperator`
876928
Proximal operator of g function
877929
x0 : :obj:`numpy.ndarray`
878-
Initial vector
879-
tau : :obj:`float` or :obj:`numpy.ndarray`, optional
930+
Initial vector (not required when ``gfirst=False``, can pass ``None``)
931+
tau : :obj:`float` or :obj:`numpy.ndarray`
880932
Positive scalar weight, which should satisfy the following condition
881933
to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is
882934
the Lipschitz constant of :math:`\nabla f`. Finally note that
883935
:math:`\tau` can be chosen to be a vector of size ``niter`` such that
884936
different :math:`\tau` is used at different iterations (i.e., continuation
885937
strategy)
886-
niter : :obj:`int`, optional
938+
niter : :obj:`int`
887939
Number of iterations of iterative scheme
888940
z0 : :obj:`numpy.ndarray`, optional
889941
Initial z vector (not required when ``gfirst=True``)
@@ -905,6 +957,11 @@ def HQS(
905957
z : :obj:`numpy.ndarray`
906958
Inverted second model
907959
960+
Raises
961+
------
962+
ValueError
963+
If both ``x0`` and ``z0`` are set to ``None``
964+
908965
Notes
909966
-----
910967
The HQS algorithm can be expressed by the following recursion [1]_:
@@ -931,9 +988,13 @@ def HQS(
931988
4, 7, pp. 932-946, 1995.
932989
933990
"""
991+
# initialize variables
992+
x, z = _x0z0_init(x0, z0)
993+
ncp = get_array_module(x)
994+
934995
# check if tau is a vector
935-
tau = np.asarray(tau, dtype=float)
936-
if tau.size == 1.0:
996+
tau = ncp.asarray(tau, dtype=float)
997+
if tau.size == 1:
937998
tau_print = str(tau)
938999
tau = tau * np.ones(niter)
9391000
else:
@@ -951,11 +1012,7 @@ def HQS(
9511012
head = " Itn x[0] f g J = f + g"
9521013
print(head)
9531014

954-
x = x0.copy()
955-
if z0 is not None:
956-
z = z0.copy()
957-
else:
958-
z = np.zeros_like(x)
1015+
# run iterations
9591016
for iiter in range(niter):
9601017
if gfirst:
9611018
z = proxg.prox(x, tau[iiter])
@@ -994,6 +1051,7 @@ def ADMM(
9941051
x0: NDArray,
9951052
tau: float,
9961053
niter: int = 10,
1054+
z0: Optional[NDArray] = None,
9971055
gfirst: bool = False,
9981056
callback: Optional[Callable[..., None]] = None,
9991057
callbackz: bool = False,
@@ -1036,13 +1094,15 @@ def ADMM(
10361094
proxg : :obj:`pyproximal.ProxOperator`
10371095
Proximal operator of g function
10381096
x0 : :obj:`numpy.ndarray`
1039-
Initial vector
1040-
tau : :obj:`float`, optional
1097+
Initial vector (not required when ``gfirst=False``, can pass ``None``)
1098+
tau : :obj:`float`
10411099
Positive scalar weight, which should satisfy the following condition
10421100
to guarantees convergence: :math:`\tau \in (0, 1/L]` where ``L`` is
10431101
the Lipschitz constant of :math:`\nabla f`.
1044-
niter : :obj:`int`, optional
1102+
niter : :obj:`int`
10451103
Number of iterations of iterative scheme
1104+
z0 : :obj:`numpy.ndarray`, optional
1105+
Initial z vector (not required when ``gfirst=True``)
10461106
gfirst : :obj:`bool`, optional
10471107
Apply Proximal of operator ``g`` first (``True``) or Proximal of
10481108
operator ``f`` first (``False``)
@@ -1066,6 +1126,11 @@ def ADMM(
10661126
ADMML2: ADMM with L2 misfit function
10671127
LinearizedADMM: Linearized ADMM
10681128
1129+
Raises
1130+
------
1131+
ValueError
1132+
If both ``x0`` and ``z0`` are set to ``None``
1133+
10691134
Notes
10701135
-----
10711136
The ADMM algorithm can be expressed by the following recursion [1]_:
@@ -1087,6 +1152,11 @@ def ADMM(
10871152
Learning, 3 (1), 1-122. https://doi.org/10.1561/2200000016.
10881153
10891154
"""
1155+
# initialize variables
1156+
x, z = _x0z0_init(x0, z0)
1157+
ncp = get_array_module(x)
1158+
u = ncp.zeros_like(x)
1159+
10901160
if show:
10911161
tstart = time.time()
10921162
print(
@@ -1099,8 +1169,7 @@ def ADMM(
10991169
head = " Itn x[0] f g J = f + g"
11001170
print(head)
11011171

1102-
x = x0.copy()
1103-
u = z = np.zeros_like(x)
1172+
# run iterations
11041173
for iiter in range(niter):
11051174
if gfirst:
11061175
z = proxg.prox(x + u, tau)
@@ -1141,6 +1210,7 @@ def ADMML2(
11411210
x0: NDArray,
11421211
tau: float,
11431212
niter: int = 10,
1213+
z0: Optional[NDArray] = None,
11441214
gfirst: bool = False,
11451215
callback: Optional[Callable[[NDArray], None]] = None,
11461216
show: bool = False,
@@ -1171,11 +1241,13 @@ def ADMML2(
11711241
Linear operator of regularization term
11721242
x0 : :obj:`numpy.ndarray`
11731243
Initial vector
1174-
tau : :obj:`float`, optional
1244+
tau : :obj:`float`
11751245
Positive scalar weight, which should satisfy the following condition
11761246
to guarantees convergence: :math:`\tau \in (0, 1/\lambda_{max}(\mathbf{A}^H\mathbf{A})]`.
11771247
niter : :obj:`int`, optional
11781248
Number of iterations of iterative scheme
1249+
z0 : :obj:`numpy.ndarray`
1250+
Initial auxiliary vector. If ``None``, initialized to ``A @ x0``.
11791251
gfirst : :obj:`bool`, optional
11801252
Apply Proximal of operator ``g`` first (``True``) or Proximal of
11811253
operator ``f`` first (``False``)
@@ -1195,6 +1267,11 @@ def ADMML2(
11951267
z : :obj:`numpy.ndarray`
11961268
Inverted second model
11971269
1270+
Raises
1271+
------
1272+
ValueError
1273+
If both ``x0`` and ``z0`` are set to ``None`` or ``x0`` is set to None
1274+
11981275
See Also
11991276
--------
12001277
ADMM: ADMM
@@ -1212,6 +1289,11 @@ def ADMML2(
12121289
\mathbf{u}^{k+1} = \mathbf{u}^{k} + \mathbf{Ax}^{k+1} - \mathbf{z}^{k+1}
12131290
12141291
"""
1292+
# initialize variables
1293+
x, z = _x0z0_init(x0, z0, A, Opname="A")
1294+
ncp = get_array_module(x)
1295+
u = ncp.zeros_like(z)
1296+
12151297
if show:
12161298
tstart = time.time()
12171299
print(
@@ -1223,9 +1305,8 @@ def ADMML2(
12231305
head = " Itn x[0] f g J = f + g"
12241306
print(head)
12251307

1308+
# run iterations
12261309
sqrttau = 1.0 / sqrt(tau)
1227-
x = x0.copy()
1228-
u = z = np.zeros(A.shape[0], dtype=A.dtype)
12291310
for iiter in range(niter):
12301311
if gfirst:
12311312
Ax = A @ x
@@ -1297,6 +1378,7 @@ def LinearizedADMM(
12971378
tau: float,
12981379
mu: float,
12991380
niter: int = 10,
1381+
z0: Optional[NDArray] = None,
13001382
callback: Optional[Callable[[NDArray], None]] = None,
13011383
show: bool = False,
13021384
) -> Tuple[NDArray, NDArray]:
@@ -1333,6 +1415,8 @@ def LinearizedADMM(
13331415
\tau/\lambda_{max}(\mathbf{A}^H\mathbf{A})]`.
13341416
niter : :obj:`int`, optional
13351417
Number of iterations of iterative scheme
1418+
z0 : :obj:`numpy.ndarray`
1419+
Initial auxiliary vector. If ``None``, initialized to ``A @ x0``.
13361420
callback : :obj:`callable`, optional
13371421
Function with signature (``callback(x)``) to call after each iteration
13381422
where ``x`` is the current model vector
@@ -1346,6 +1430,11 @@ def LinearizedADMM(
13461430
z : :obj:`numpy.ndarray`
13471431
Inverted second model
13481432
1433+
Raises
1434+
------
1435+
ValueError
1436+
If both ``x0`` and ``z0`` are set to ``None``
1437+
13491438
See Also
13501439
--------
13511440
ADMM: ADMM
@@ -1368,6 +1457,12 @@ def LinearizedADMM(
13681457
in Optimization. 2013.
13691458
13701459
"""
1460+
# initialize variables
1461+
x, z = _x0z0_init(x0, z0, A, Opname="A")
1462+
Ax = A.matvec(x) if z0 is None else z
1463+
ncp = get_array_module(x)
1464+
u = ncp.zeros_like(z)
1465+
13711466
if show:
13721467
tstart = time.time()
13731468
print(
@@ -1381,9 +1476,8 @@ def LinearizedADMM(
13811476
)
13821477
head = " Itn x[0] f g J = f + g"
13831478
print(head)
1384-
x = x0.copy()
1385-
Ax = A.matvec(x)
1386-
u = z = np.zeros_like(Ax)
1479+
1480+
# run iterations
13871481
for iiter in range(niter):
13881482
x = proxf.prox(x - mu / tau * A.rmatvec(Ax - z + u), mu)
13891483
Ax = A.matvec(x)

‎pyproximal/optimization/primaldual.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def PrimalDual(
7070
Stepsize of subgradient of :math:`g^*`. This can be constant
7171
or function of iterations (in the latter cases provided as np.ndarray)
7272
y0 : :obj:`numpy.ndarray`
73-
Initial auxiliary vector
73+
Initial auxiliary vector. If ``None``, set to zero
7474
z : :obj:`numpy.ndarray`, optional
7575
Additional vector
7676
theta : :obj:`float`
@@ -167,9 +167,12 @@ def PrimalDual(
167167
head = " Itn x[0] f g z^x J = f + g + z^x"
168168
print(head)
169169

170+
# initialize variables
170171
x = x0.copy()
171-
xhat = x.copy()
172172
y = y0.copy() if y0 is not None else ncp.zeros(A.shape[0], dtype=x.dtype)
173+
xhat = x.copy()
174+
175+
# run iterations
173176
for iiter in range(niter):
174177
xold = x.copy()
175178
if gfirst:

0 commit comments

Comments
 (0)