55
66import numpy as np
77from 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
99from pylops .utils .typing import NDArray
1010
1111from 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+
53105def 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 )
0 commit comments