def __init__(self, xstep, dstep, opt=None, isc=None): """ Parameters ---------- xstep : bpdn (or similar interface) object Object handling X update step dstep : cmod (or similar interface) object Object handling D update step opt : :class:`DictLearn.Options` object Algorithm options isc : :class:`IterStatsConfig` object Iteration statistics and header display configuration """ if opt is None: opt = DictLearn.Options() self.opt = opt if isc is None: isc = IterStatsConfig(isfld=[ 'Iter', 'ObjFunX', 'XPrRsdl', 'XDlRsdl', 'XRho', 'ObjFunD', 'DPrRsdl', 'DDlRsdl', 'DRho', 'Time' ], isxmap={ 'ObjFunX': 'ObjFun', 'XPrRsdl': 'PrimalRsdl', 'XDlRsdl': 'DualRsdl', 'XRho': 'Rho' }, isdmap={ 'ObjFunD': 'DFid', 'DPrRsdl': 'PrimalRsdl', 'DDlRsdl': 'DualRsdl', 'DRho': 'Rho' }, evlmap={}, hdrtxt=[ 'Itn', 'FncX', 'r_X', 's_X', u('ρ_X'), 'FncD', 'r_D', 's_D', u('ρ_D') ], hdrmap={ 'Itn': 'Iter', 'FncX': 'ObjFunX', 'r_X': 'XPrRsdl', 's_X': 'XDlRsdl', u('ρ_X'): 'XRho', 'FncD': 'ObjFunD', 'r_D': 'DPrRsdl', 's_D': 'DDlRsdl', u('ρ_D'): 'DRho' }) self.isc = isc self.xstep = xstep self.dstep = dstep self.itstat = [] self.j = 0
def hdrval(cls): """Construct dictionary mapping display column title to IterationStats entries. """ hdrmap = {'Itn': 'Iter'} hdrmap.update(cls.hdrval_objfun) hdrmap.update({'r': 'PrimalRsdl', 's': 'DualRsdl', u('ρ'): 'Rho'}) return hdrmap
def hdrtxt(xmethod, dmethod, opt): """Return ``hdrtxt`` argument for ``.IterStatsConfig`` initialiser. """ txt = ['Itn', 'Fnc', 'DFid', u('ℓ1'), 'Cnstr'] if xmethod == 'admm': txt.extend(['r_X', 's_X', u('ρ_X')]) else: if opt['CBPDN', 'BackTrack', 'Enabled']: txt.extend(['F_X', 'Q_X', 'It_X', 'L_X']) else: txt.append('L_X') if dmethod != 'fista': txt.extend(['r_D', 's_D', u('ρ_D')]) else: if opt['CCMOD', 'BackTrack', 'Enabled']: txt.extend(['F_D', 'Q_D', 'It_D', 'L_D']) else: txt.append('L_D') return txt
def hdrmap(xmethod, dmethod, opt): """Return ``hdrmap`` argument for ``.IterStatsConfig`` initialiser. """ hdr = { 'Itn': 'Iter', 'Fnc': 'ObjFun', 'DFid': 'DFid', u('ℓ1'): 'RegL1', 'Cnstr': 'Cnstr' } if xmethod == 'admm': hdr.update({'r_X': 'XPrRsdl', 's_X': 'XDlRsdl', u('ρ_X'): 'XRho'}) else: if opt['CBPDN', 'BackTrack', 'Enabled']: hdr.update({ 'F_X': 'X_F_Btrack', 'Q_X': 'X_Q_Btrack', 'It_X': 'X_ItBt', 'L_X': 'X_L' }) else: hdr.update({'L_X': 'X_L'}) if dmethod != 'fista': hdr.update({'r_D': 'DPrRsdl', 's_D': 'DDlRsdl', u('ρ_D'): 'DRho'}) else: if opt['CCMOD', 'BackTrack', 'Enabled']: hdr.update({ 'F_D': 'D_F_Btrack', 'Q_D': 'D_Q_Btrack', 'It_D': 'D_ItBt', 'L_D': 'D_L' }) else: hdr.update({'L_D': 'D_L'}) return hdr
class ConvBPDN(fista.FISTADFT): r""" Base class for FISTA algorithm for the Convolutional BPDN (CBPDN) :cite:`garcia-2018-convolutional1` problem. | .. inheritance-diagram:: ConvBPDN :parts: 2 | The generic problem form is .. math:: \mathrm{argmin}_\mathbf{x} \; f( \{ \mathbf{x}_m \} ) + \lambda g( \{ \mathbf{x}_m \} ) where :math:`f = (1/2) \left\| \sum_m \mathbf{d}_m * \mathbf{x}_m - \mathbf{s} \right\|_2^2`, and :math:`g(\cdot)` is a penalty term or the indicator function of a constraint; with input image :math:`\mathbf{s}`, dictionary filters :math:`\mathbf{d}_m`, and coefficient maps :math:`\mathbf{x}_m`. It is solved via the FISTA formulation Proximal step .. math:: \mathbf{x}_k = \mathrm{prox}_{t_k}(g) (\mathbf{y}_k - 1/L \nabla f(\mathbf{y}_k) ) \;\;. Combination step .. math:: \mathbf{y}_{k+1} = \mathbf{x}_k + \left( \frac{t_k - 1}{t_{k+1}} \right) (\mathbf{x}_k - \mathbf{x}_{k-1}) \;\;, with :math:`t_{k+1} = \frac{1 + \sqrt{1 + 4 t_k^2}}{2}`. After termination of the :meth:`solve` method, attribute :attr:`itstat` is a list of tuples representing statistics of each iteration. The fields of the named tuple ``IterationStats`` are: ``Iter`` : Iteration number ``ObjFun`` : Objective function value ``DFid`` : Value of data fidelity term :math:`(1/2) \| \sum_m \mathbf{d}_m * \mathbf{x}_m - \mathbf{s} \|_2^2` ``RegL1`` : Value of regularisation term :math:`\sum_m \| \mathbf{x}_m \|_1` ``Rsdl`` : Residual ``L`` : Inverse of gradient step parameter ``Time`` : Cumulative run time """ class Options(fista.FISTADFT.Options): r"""ConvBPDN algorithm options Options include all of those defined in :class:`.fista.FISTADFT.Options`, together with additional options: ``NonNegCoef`` : Flag indicating whether to force solution to be non-negative. ``NoBndryCross`` : Flag indicating whether all solution coefficients corresponding to filters crossing the image boundary should be forced to zero. ``L1Weight`` : An array of weights for the :math:`\ell_1` norm. The array shape must be such that the array is compatible for multiplication with the X/Y variables. If this option is defined, the regularization term is :math:`\lambda \sum_m \| \mathbf{w}_m \odot \mathbf{x}_m \|_1` where :math:`\mathbf{w}_m` denotes slices of the weighting array on the filter index axis. """ defaults = copy.deepcopy(fista.FISTADFT.Options.defaults) defaults.update({'NonNegCoef': False, 'NoBndryCross': False}) defaults.update({'L1Weight': 1.0}) defaults.update({'L': 500.0}) def __init__(self, opt=None): """ Parameters ---------- opt : dict or None, optional (default None) ConvBPDN algorithm options """ if opt is None: opt = {} fista.FISTADFT.Options.__init__(self, opt) def __setitem__(self, key, value): """Set options.""" fista.FISTADFT.Options.__setitem__(self, key, value) itstat_fields_objfn = ('ObjFun', 'DFid', 'RegL1') hdrtxt_objfn = ('Fnc', 'DFid', u('Regℓ1')) hdrval_objfun = {'Fnc': 'ObjFun', 'DFid': 'DFid', u('Regℓ1'): 'RegL1'} def __init__(self, D, S, lmbda=None, opt=None, dimK=None, dimN=2): """ This class supports an arbitrary number of spatial dimensions, `dimN`, with a default of 2. The input dictionary `D` is either `dimN` + 1 dimensional, in which case each spatial component (image in the default case) is assumed to consist of a single channel, or `dimN` + 2 dimensional, in which case the final dimension is assumed to contain the channels (e.g. colour channels in the case of images). The input signal set `S` is either `dimN` dimensional (no channels, only one signal), `dimN` + 1 dimensional (either multiple channels or multiple signals), or `dimN` + 2 dimensional (multiple channels and multiple signals). Determination of problem dimensions is handled by :class:`.cnvrep.CSC_ConvRepIndexing`. | **Call graph** .. image:: ../_static/jonga/fista_cbpdn_init.svg :width: 20% :target: ../_static/jonga/fista_cbpdn_init.svg | Parameters ---------- D : array_like Dictionary array S : array_like Signal array lmbda : float Regularisation parameter opt : :class:`ConvBPDN.Options` object Algorithm options dimK : 0, 1, or None, optional (default None) Number of dimensions in input signal corresponding to multiple independent signals dimN : int, optional (default 2) Number of spatial/temporal dimensions """ # Set default options if none specified if opt is None: opt = ConvBPDN.Options() # Infer problem dimensions and set relevant attributes of self if not hasattr(self, 'cri'): self.cri = cr.CSC_ConvRepIndexing(D, S, dimK=dimK, dimN=dimN) # Set dtype attribute based on S.dtype and opt['DataType'] self.set_dtype(opt, S.dtype) # Set default lambda value if not specified if lmbda is None: cri = cr.CSC_ConvRepIndexing(D, S, dimK=dimK, dimN=dimN) Df = sl.rfftn(D.reshape(cri.shpD), cri.Nv, axes=cri.axisN) Sf = sl.rfftn(S.reshape(cri.shpS), axes=cri.axisN) b = np.conj(Df) * Sf lmbda = 0.1 * abs(b).max() # Set l1 term scaling and weight array self.lmbda = self.dtype.type(lmbda) self.wl1 = np.asarray(opt['L1Weight'], dtype=self.dtype) # Call parent class __init__ self.Xf = None xshape = self.cri.shpX super(ConvBPDN, self).__init__(xshape, S.dtype, opt) # Reshape D and S to standard layout self.D = np.asarray(D.reshape(self.cri.shpD), dtype=self.dtype) self.S = np.asarray(S.reshape(self.cri.shpS), dtype=self.dtype) # Compute signal in DFT domain self.Sf = sl.rfftn(self.S, None, self.cri.axisN) # Create byte aligned arrays for FFT calls self.Y = self.X.copy() self.X = sl.pyfftw_empty_aligned(self.Y.shape, dtype=self.dtype) self.X[:] = self.Y # Initialise auxiliary variable Vf: Create byte aligned arrays # for FFT calls self.Vf = sl.pyfftw_rfftn_empty_aligned(self.X.shape, self.cri.axisN, self.dtype) self.Xf = sl.rfftn(self.X, None, self.cri.axisN) self.Yf = self.Xf.copy() self.store_prev() self.Yfprv = self.Yf.copy() + 1e5 self.setdict() # Initialization needed for back tracking (if selected) self.postinitialization_backtracking_DFT() def setdict(self, D=None): """Set dictionary array.""" if D is not None: self.D = np.asarray(D, dtype=self.dtype) self.Df = sl.rfftn(self.D, self.cri.Nv, self.cri.axisN) def getcoef(self): """Get final coefficient array.""" return self.X def eval_grad(self): """Compute gradient in Fourier domain.""" # Compute D X - S Ryf = self.eval_Rf(self.Yf) # Compute D^H Ryf gradf = np.conj(self.Df) * Ryf # Multiple channel signal, multiple channel dictionary if self.cri.Cd > 1: gradf = np.sum(gradf, axis=self.cri.axisC, keepdims=True) return gradf def eval_Rf(self, Vf): """Evaluate smooth term in Vf.""" return sl.inner(self.Df, Vf, axis=self.cri.axisM) - self.Sf def eval_proxop(self, V): """Compute proximal operator of :math:`g`.""" return sl.shrink1(V, (self.lmbda / self.L) * self.wl1) def rsdl(self): """Compute fixed point residual in Fourier domain.""" diff = self.Xf - self.Yfprv return sl.rfl2norm2(diff, self.X.shape, axis=self.cri.axisN) def eval_objfn(self): """Compute components of objective function as well as total contribution to objective function. """ dfd = self.obfn_dfd() reg = self.obfn_reg() obj = dfd + reg[0] return (obj, dfd) + reg[1:] def obfn_dfd(self): r"""Compute data fidelity term :math:`(1/2) \| \sum_m \mathbf{d}_m * \mathbf{x}_m - \mathbf{s} \|_2^2`. This function takes into account the unnormalised DFT scaling, i.e. given that the variables are the DFT of multi-dimensional arrays computed via :func:`rfftn`, this returns the data fidelity term in the original (spatial) domain. """ Ef = self.eval_Rf(self.Xf) return sl.rfl2norm2(Ef, self.S.shape, axis=self.cri.axisN) / 2.0 def obfn_reg(self): """Compute regularisation term and contribution to objective function. """ rl1 = np.linalg.norm((self.wl1 * self.X).ravel(), 1) return (self.lmbda * rl1, rl1) def obfn_f(self, Xf=None): r"""Compute data fidelity term :math:`(1/2) \| \sum_m \mathbf{d}_m * \mathbf{x}_m - \mathbf{s} \|_2^2` This is used for backtracking. Since the backtracking is computed in the DFT, it is important to preserve the DFT scaling. """ if Xf is None: Xf = self.Xf Rf = self.eval_Rf(Xf) return 0.5 * np.linalg.norm(Rf.flatten(), 2)**2 def reconstruct(self, X=None): """Reconstruct representation.""" if X is None: X = self.X Xf = sl.rfftn(X, None, self.cri.axisN) Sf = np.sum(self.Df * Xf, axis=self.cri.axisM) return sl.irfftn(Sf, self.cri.Nv, self.cri.axisN)
def hdrtxt(cls): """Construct tuple of status display column titles.""" return ('Itn', ) + cls.hdrtxt_objfn + ('r', 's', u('ρ'))
def solve(self): """Start (or re-start) optimisation. This method implements the framework for the alternation between `X` and `D` updates in a dictionary learning algorithm. If option ``Verbose`` is ``True``, the progress of the optimisation is displayed at every iteration. At termination of this method, attribute :attr:`itstat` is a list of tuples representing statistics of each iteration. Attribute :attr:`timer` is an instance of :class:`.util.Timer` that provides the following labelled timers: ``init``: Time taken for object initialisation by :meth:`__init__` ``solve``: Total time taken by call(s) to :meth:`solve` ``solve_wo_func``: Total time taken by call(s) to :meth:`solve`, excluding time taken to compute functional value and related iteration statistics """ # Construct tuple of status display column titles and set status # display strings hdrtxt = ['Itn', 'Fnc', 'DFid', u('Regℓ1')] hdrstr, fmtstr, nsep = common.solve_status_str( hdrtxt, fwdth0=type(self).fwiter, fprec=type(self).fpothr) # Print header and separator strings if self.opt['Verbose']: if self.opt['StatusHeader']: print(hdrstr) print("-" * nsep) pobjs = [] X = np.transpose(self.xstep.S.squeeze(), (2, 1, 0))[None] n_trials, n_channels, *sig_support = X.shape d_hat = np.transpose(self.getdict().squeeze(), (3, 2, 1, 0)) n_atoms, n_channels, *atom_support = d_hat.shape z_slice = tuple([None, Ellipsis] + [ slice(size_ax - size_atom_ax + 1) for size_ax, size_atom_ax in zip(sig_support, atom_support) ]) Z_hat = self.getcoef().squeeze().swapaxes(0, 2)[z_slice] pobjs.append( compute_X_and_objective(X, Z_hat, d_hat, reg=self.xstep.lmbda)) # Reset timer self.timer.start(['solve', 'solve_wo_eval']) # Create process pool if self.nproc > 0: self.pool = mp.Pool(processes=self.nproc) for self.j in range(self.j, self.j + self.opt['MaxMainIter']): # Perform a set of update steps self.step() # Evaluate functional self.timer.stop('solve_wo_eval') fnev = self.evaluate() self.timer.start('solve_wo_eval') # Record iteration stats tk = self.timer.elapsed('solve') itst = self.IterationStats(*((self.j, ) + fnev + (tk, ))) self.itstat.append(itst) self.timer.stop(['solve', 'solve_wo_eval']) d_hat = np.transpose(self.getdict().squeeze(), (3, 2, 1, 0)) Z_hat = self.getcoef().squeeze().swapaxes(0, 2)[z_slice] pobjs.append( compute_X_and_objective(X, Z_hat, d_hat, reg=self.xstep.lmbda)) tk = self.timer.elapsed('solve') print("[Wohlberg:PROGRESS] Iteration {} - {:.3e} ({:.0f}s)".format( self.j, pobjs[-1], tk)) self.timer.start(['solve', 'solve_wo_eval']) # Display iteration stats if Verbose option enabled # if self.opt['Verbose']: # print(fmtstr % itst[:-1]) # Call callback function if defined if self.opt['Callback'] is not None: if self.opt['Callback'](self): break # Clean up process pool if self.nproc > 0: self.pool.close() self.pool.join() # Increment iteration count self.j += 1 # Record solve time self.timer.stop(['solve', 'solve_wo_eval']) # Print final separator string if Verbose option enabled if self.opt['Verbose'] and self.opt['StatusHeader']: print("-" * nsep) # Return final dictionary return self.getdict(), pobjs
def solve(self): """Start (or re-start) optimisation. This method implements the framework for the alternation between `X` and `D` updates in a dictionary learning algorithm. If option ``Verbose`` is ``True``, the progress of the optimisation is displayed at every iteration. At termination of this method, attribute :attr:`itstat` is a list of tuples representing statistics of each iteration. Attribute :attr:`timer` is an instance of :class:`.util.Timer` that provides the following labelled timers: ``init``: Time taken for object initialisation by :meth:`__init__` ``solve``: Total time taken by call(s) to :meth:`solve` ``solve_wo_func``: Total time taken by call(s) to :meth:`solve`, excluding time taken to compute functional value and related iteration statistics """ # Construct tuple of status display column titles and set status # display strings hdrtxt = ['Itn', 'Fnc', 'DFid', u('Regℓ1')] hdrstr, fmtstr, nsep = common.solve_status_str( hdrtxt, fwdth0=type(self).fwiter, fprec=type(self).fpothr) # Print header and separator strings if self.opt['Verbose']: if self.opt['StatusHeader']: print(hdrstr) print("-" * nsep) # Reset timer self.timer.start(['solve', 'solve_wo_eval']) # Create process pool if self.nproc > 0: self.pool = mp.Pool(processes=self.nproc) for self.j in range(self.j, self.j + self.opt['MaxMainIter']): # Perform a set of update steps self.step() # Evaluate functional self.timer.stop('solve_wo_eval') fnev = self.evaluate() self.timer.start('solve_wo_eval') # Record iteration stats tk = self.timer.elapsed('solve') itst = self.IterationStats(*((self.j, ) + fnev + (tk, ))) self.itstat.append(itst) # Display iteration stats if Verbose option enabled if self.opt['Verbose']: print(fmtstr % itst[:-1]) # Call callback function if defined if self.opt['Callback'] is not None: if self.opt['Callback'](self): break # Clean up process pool if self.nproc > 0: self.pool.close() self.pool.join() # Increment iteration count self.j += 1 # Record solve time self.timer.stop(['solve', 'solve_wo_eval']) # Print final separator string if Verbose option enabled if self.opt['Verbose'] and self.opt['StatusHeader']: print("-" * nsep) # Return final dictionary return self.getdict()
class ParConvBPDN(GenericConvBPDN): r""" Parallel ADMM algorithm for Convolutional BPDN (CBPDN) with or without a spatial mask :cite:`skau-2018-fast`. | .. inheritance-diagram:: ParConvBPDN :parts: 2 | Solve the optimisation problem .. math:: \mathrm{argmin}_\mathbf{x} \; (1/2) \left\| W \left(\sum_m \mathbf{d}_m * \mathbf{x}_m - \mathbf{s}\right) \right\|_2^2 + \lambda \sum_m \| \mathbf{x}_m \|_1 \;\;, where :math:`W` is a mask array, via the ADMM problem .. math:: \mathrm{argmin}_{\mathbf{x},\mathbf{y}_0,\mathbf{y}_1} \; (1/2) \| W \left( \sum_l \mathbf{y}_{0,l} - \mathbf{s} \right) \|_2^2 + \lambda \| \mathbf{y}_1 \|_1 \;\text{such that}\; \left( \begin{array}{c} D_{G_0} \\ \vdots \\ D_{G_{L-1}} \\ \alpha I \end{array} \right) \mathbf{x} - \left( \begin{array}{c} \mathbf{y}_{0,0} \\ \vdots \\ \mathbf{y}_{0,L-1} \\ \alpha \mathbf{y}_1 \end{array} \right) = \left( \begin{array}{c} \mathbf{0} \\ \vdots \\ \mathbf{0} \\ \mathbf{0} \end{array} \right) \;\;, where the :math:`M` dictionary filters are partitioned into :math:`L` groups, :math:`\{G_l\}_{l \in \{0,\dots,L-1\}}` where .. math:: G_i \cap G_j = \emptyset \text{ for } i \neq j \text{ and } \bigcup_l G_l = \{0, \dots, M-1\} \;, and :math:`D_{G_l}` is a linear operator such that :math:`D_{G_l} \mathbf{x} = \sum_{g \in G_l} \mathbf{d}_g * \mathbf{x}_g`. Multi-image and multi-channel problems are also supported. The multi-image problem is .. math:: \mathrm{argmin}_\mathbf{x} \; (1/2) \sum_k \left\| W_k \left( \sum_m \mathbf{d}_m * \mathbf{x}_{k,m} - \mathbf{s}_k \right) \right\|_2^2 + \lambda \sum_k \sum_m \| \mathbf{x}_{k,m} \|_1 with input images :math:`\mathbf{s}_k`, masks :math:`W_k`, and coefficient maps :math:`\mathbf{x}_{k,m}`. The multi-channel problem with input image channels :math:`\mathbf{s}_c` and a multi-channel mask :math:`W_c` is either .. math:: \mathrm{argmin}_\mathbf{x} \; (1/2) \sum_c \left\| W_c \left( \sum_m \mathbf{d}_m * \mathbf{x}_{c,m} - \mathbf{s}_c \right) \right\|_2^2 + \lambda \sum_c \sum_m \| \mathbf{x}_{c,m} \|_1 with single-channel dictionary filters :math:`\mathbf{d}_m` and multi-channel coefficient maps :math:`\mathbf{x}_{c,m}`, or .. math:: \mathrm{argmin}_\mathbf{x} \; (1/2) \sum_c \left\| W_c \left( \sum_m \mathbf{d}_{c,m} * \mathbf{x}_m - \mathbf{s}_c \right) \right\|_2^2 + \lambda \sum_m \| \mathbf{x}_m \|_1 with multi-channel dictionary filters :math:`\mathbf{d}_{c,m}` and single-channel coefficient maps :math:`\mathbf{x}_m`. After termination of the :meth:`solve` method, AttributeError :attr:`itstat` is a list of tuples representing statistics of each iteration. The fields of the named tuple ``IterationStats`` are: ``Iter`` : Iteration number ``ObjFun`` : Objective function value ``DFid`` : Value of data fidelity term :math:`(1/2) \| W \left( \sum_m \mathbf{d}_m * \mathbf{x}_m - \mathbf{s} \right) \|_2^2` ``RegL1`` : Value of regularisation term :math:`\sum_m \| \mathbf{x}_m \|_1` ``PrimalRsdl`` : Norm of primal residual ``DualRsdl`` : Norm of dual residual ``EpsPrimal`` : Primal residual stopping tolerance :math:`\epsilon_{\mathrm{pri}}` ``EpsDual`` : Dual residual stopping tolerance :math:`\epsilon_{\mathrm{dua}}` ``Rho`` : Penalty parameter ``XSlvRelRes`` : Not Implemented (relative residual of X step solver) ``Time`` : Cumulative run time """ class Options(GenericConvBPDN.Options): r"""ParConvBPDN algorithm options Options include all of those defined in :class:`.admm.ADMMEqual.Options`, together with additional options: ``alpha`` : A float indicating the relative weight between the constraint :math:`D_{G_l} \mathbf{x} = \mathbf{y}_{0,l}` and :math:`\alpha \mathbf{x} = \mathbf{y}_1`. None value effectively defaults to no weight or :math:`\alpha = 1`. ``Y0`` : Initial value for :math:`\mathbf{y}_0`. ``U0`` : Initial value for :math:`\mathbf{u}_0`. ``Y1`` : Initial value for :math:`\mathbf{y}_1`. ``U1`` : Initial value for :math:`\mathbf{u}_1`. and the exceptions: ``AutoRho`` : Not implemented. ``LinSolveCheck`` : Not implemented. """ defaults = copy.deepcopy(GenericConvBPDN.Options.defaults) defaults.update({ 'L1Weight': 1.0, 'alpha': None, 'Y1': None, 'U1': None }) def __init__(self, opt=None): """ Parameters ---------- opt : dict or None, optional (default None) ParConvBPDN algorithm options """ if opt is None: opt = {} GenericConvBPDN.Options.__init__(self, opt) itstat_fields_objfn = ('ObjFun', 'DFid', 'RegL1') hdrtxt_objfn = ('Fnc', 'DFid', u('Regl1')) hdrval_objfun = {'Fnc': 'ObjFun', 'DFid': 'DFid', u('Regl1'): 'RegL1'} def __init__(self, D, S, lmbda=None, W=None, opt=None, nproc=None, ngrp=None, dimK=None, dimN=2): """ Parameters ---------- D : array_like Dictionary matrix S : array_like Signal vector or matrix lmbda : float Regularisation parameter W : array_like Mask array. The array shape must be such that the array is compatible for multiplication with input array S (see :func:`.cnvrep.mskWshape` for more details). opt : :class:`ParConvBPDN.Options` object Algorithm options nproc : int Number of processes ngrp : int Number of groups in partition of filter indices dimK : 0, 1, or None, optional (default None) Number of dimensions in input signal corresponding to multiple independent signals dimN : int, optional (default 2) Number of spatial dimensions """ self.pool = None # Set default options if none specified if opt is None: opt = ParConvBPDN.Options() # Set dtype attribute based on S.dtype and opt['DataType'] self.set_dtype(opt, S.dtype) # Set default lambda value if not specified if lmbda is None: cri = cr.CSC_ConvRepIndexing(D, S, dimK=dimK, dimN=dimN) Df = sl.rfftn(D.reshape(cri.shpD), cri.Nv, axes=cri.axisN) Sf = sl.rfftn(S.reshape(cri.shpS), axes=cri.axisN) b = np.conj(Df) * Sf lmbda = 0.1 * abs(b).max() # Set l1 term scaling and weight array self.lmbda = self.dtype.type(lmbda) # Set penalty parameter self.set_attr('rho', opt['rho'], dval=(50.0 * self.lmbda + 1.0), dtype=self.dtype) self.set_attr('alpha', opt['alpha'], dval=1.0, dtype=self.dtype) # Set rho_xi attribute (see Sec. VI.C of wohlberg-2015-adaptive) # if self.lmbda != 0.0: # rho_xi = (1.0 + (18.3)**(np.log10(self.lmbda) + 1.0)) # else: # rho_xi = 1.0 # self.set_attr('rho_xi', opt['AutoRho', 'RsdlTarget'], dval=rho_xi, # dtype=self.dtype) # Call parent class __init__ super(ParConvBPDN, self).__init__(D, S, opt, dimK, dimN) if nproc is None: if ngrp is None: self.nproc = min(mp.cpu_count(), self.cri.M) self.ngrp = self.nproc else: self.nproc = min(mp.cpu_count(), ngrp, self.cri.M) self.ngrp = ngrp else: if ngrp is None: self.ngrp = nproc self.nproc = nproc else: self.ngrp = ngrp self.nproc = nproc if W is None: W = np.array([1.0], dtype=self.dtype) self.W = np.asarray(W.reshape(cr.mskWshape(W, self.cri)), dtype=self.dtype) self.wl1 = np.asarray(opt['L1Weight'], dtype=self.dtype) self.wl1 = self.wl1.reshape(cr.l1Wshape(self.wl1, self.cri)) self.xrrs = None # Initialise global variables # Conv Rep Indexing and parameter values for multiprocessing global mp_nproc mp_nproc = self.nproc global mp_ngrp mp_ngrp = self.ngrp global mp_Nv mp_Nv = self.cri.Nv global mp_axisN mp_axisN = tuple(i + 1 for i in self.cri.axisN) global mp_C mp_C = self.cri.C global mp_Cd mp_Cd = self.cri.Cd global mp_axisC mp_axisC = self.cri.axisC + 1 global mp_axisM mp_axisM = 0 global mp_NonNegCoef mp_NonNegCoef = self.opt['NonNegCoef'] global mp_NoBndryCross mp_NoBndryCross = self.opt['NoBndryCross'] global mp_Dshp mp_Dshp = self.D.shape # Parameters for optimization global mp_lmbda mp_lmbda = self.lmbda global mp_rho mp_rho = self.rho global mp_alpha mp_alpha = self.alpha global mp_rlx mp_rlx = self.rlx global mp_wl1 init_mpraw('mp_wl1', np.moveaxis(self.wl1, self.cri.axisM, mp_axisM)) # Matrices used in optimization global mp_S init_mpraw('mp_S', np.moveaxis(self.S * self.W**2, self.cri.axisM, mp_axisM)) global mp_Df init_mpraw('mp_Df', np.moveaxis(self.Df, self.cri.axisM, mp_axisM)) global mp_X init_mpraw('mp_X', np.moveaxis(self.Y, self.cri.axisM, mp_axisM)) shp_X = list(mp_X.shape) global mp_Xnr mp_Xnr = mpraw_as_np(mp_X.shape, mp_X.dtype) global mp_Y0 shp_Y0 = shp_X[:] shp_Y0[0] = self.ngrp shp_Y0[mp_axisC] = mp_C if self.opt['Y0'] is not None: init_mpraw( 'Y0', np.moveaxis(self.opt['Y0'].astype(self.dtype, copy=True), self.cri.axisM, mp_axisM)) else: mp_Y0 = mpraw_as_np(shp_Y0, mp_X.dtype) global mp_Y0old mp_Y0old = mpraw_as_np(shp_Y0, mp_X.dtype) global mp_Y1 if self.opt['Y1'] is not None: init_mpraw( 'Y1', np.moveaxis(self.opt['Y1'].astype(self.dtype, copy=True), self.cri.axisM, mp_axisM)) else: mp_Y1 = mpraw_as_np(shp_X, mp_X.dtype) global mp_Y1old mp_Y1old = mpraw_as_np(shp_X, mp_X.dtype) global mp_U0 if self.opt['U0'] is not None: init_mpraw( 'U0', np.moveaxis(self.opt['U0'].astype(self.dtype, copy=True), self.cri.axisM, mp_axisM)) else: mp_U0 = mpraw_as_np(shp_Y0, mp_X.dtype) global mp_U1 if self.opt['U1'] is not None: init_mpraw( 'U1', np.moveaxis(self.opt['U1'].astype(self.dtype, copy=True), self.cri.axisM, mp_axisM)) else: mp_U1 = mpraw_as_np(shp_X, mp_X.dtype) global mp_DX mp_DX = mpraw_as_np(shp_Y0, mp_X.dtype) global mp_DXnr mp_DXnr = mpraw_as_np(shp_Y0, mp_X.dtype) # Variables used to solve the optimization efficiently global mp_inv_off_diag if self.W.ndim is self.cri.axisM + 1: init_mpraw( 'mp_inv_off_diag', np.moveaxis( -self.W**2 / (mp_rho * (mp_rho + self.W**2 * mp_ngrp)), self.cri.axisM, mp_axisM)) else: init_mpraw('mp_inv_off_diag', -self.W**2 / (mp_rho * (mp_rho + self.W**2 * mp_ngrp))) global mp_grp mp_grp = [ np.min(i) for i in np.array_split(np.array(range(self.cri.M)), mp_ngrp) ] + [ self.cri.M, ] global mp_cache if self.opt['HighMemSolve'] and self.cri.Cd == 1: mp_cache = [ sl.solvedbi_sm_c(mp_Df[k], np.conj(mp_Df[k]), mp_alpha**2, mp_axisM) for k in np.array_split(np.array(range(self.cri.M)), self.ngrp) ] else: mp_cache = [None for k in mp_grp] global mp_b shp_b = shp_Y0[:] shp_b[0] = 1 mp_b = mpraw_as_np(shp_b, mp_X.dtype) # Residual and stopping criteria variables global mp_ry0 mp_ry0 = mpraw_as_np((self.ngrp, ), mp_X.dtype) global mp_ry1 mp_ry1 = mpraw_as_np((self.ngrp, ), mp_X.dtype) global mp_sy0 mp_sy0 = mpraw_as_np((self.ngrp, ), mp_X.dtype) global mp_sy1 mp_sy1 = mpraw_as_np((self.ngrp, ), mp_X.dtype) global mp_nrmAx mp_nrmAx = mpraw_as_np((self.ngrp, ), mp_X.dtype) global mp_nrmBy mp_nrmBy = mpraw_as_np((self.ngrp, ), mp_X.dtype) global mp_nrmu mp_nrmu = mpraw_as_np((self.ngrp, ), mp_X.dtype) def solve(self): """Start (or re-start) optimisation. This method implements the framework for the iterations of an ADMM algorithm. If option ``Verbose`` is ``True``, the progress of the optimisation is displayed at every iteration. At termination of this method, attribute :attr:`itstat` is a list of tuples representing statistics of each iteration, unless option ``FastSolve`` is ``True`` and option ``Verbose`` is ``False``. Attribute :attr:`timer` is an instance of :class:`.util.Timer` that provides the following labelled timers: ``init``: Time taken for object initialisation by :meth:`__init__` ``solve``: Total time taken by call(s) to :meth:`solve` ``solve_wo_func``: Total time taken by call(s) to :meth:`solve`, excluding time taken to compute functional value and related iteration statistics ``solve_wo_rsdl`` : Total time taken by call(s) to :meth:`solve`, excluding time taken to compute functional value and related iteration statistics as well as time take to compute residuals and implemented ``AutoRho`` mechanism """ global mp_Y0old global mp_Y1old self.init_pool() fmtstr, nsep = self.display_start() # Start solve timer self.timer.start(['solve', 'solve_wo_func', 'solve_wo_rsdl']) first_iteration = self.k last_iteration = self.k + self.opt['MaxMainIter'] - 1 # Main optimisation iterations for self.k in range(self.k, self.k + self.opt['MaxMainIter']): mp_Y0old[:] = np.copy(mp_Y0) mp_Y1old[:] = np.copy(mp_Y1) # Perform the variable updates. if self.k is first_iteration: self.distribute(par_initial_stepgrp, mp_ngrp) y0astep() if self.k is last_iteration: self.distribute(par_final_stepgrp, mp_ngrp) else: self.distribute(par_stepgrp, mp_ngrp) # Compute the residual variables self.timer.stop('solve_wo_rsdl') if self.opt['AutoRho', 'Enabled'] or not self.opt['FastSolve']: self.distribute(par_compute_residuals, mp_ngrp) r = np.sqrt(np.sum(mp_ry0) + np.sum(mp_ry1)) s = np.sqrt(np.sum(mp_sy0) + np.sum(mp_sy1)) epri = np.sqrt(self.Nc) * self.opt['AbsStopTol'] + \ np.max([np.sqrt(np.sum(mp_nrmAx)), np.sqrt(np.sum(mp_nrmBy))]) * self.opt['RelStopTol'] edua = np.sqrt(self.Nx) * self.opt['AbsStopTol'] + \ np.sqrt(np.sum(mp_nrmu)) * self.opt['RelStopTol'] # Compute and record other iteration statistics and # display iteration stats if Verbose option enabled self.timer.stop(['solve_wo_func', 'solve_wo_rsdl']) if not self.opt['FastSolve']: itst = self.iteration_stats(self.k, r, s, epri, edua) self.itstat.append(itst) self.display_status(fmtstr, itst) self.timer.start(['solve_wo_func', 'solve_wo_rsdl']) # Automatic rho adjustment # self.timer.stop('solve_wo_rsdl') # if self.opt['AutoRho', 'Enabled'] or not self.opt['FastSolve']: # self.update_rho(self.k, r, s) # self.timer.start('solve_wo_rsdl') # Call callback function if defined if self.opt['Callback'] is not None: if self.opt['Callback'](self): break # Stop if residual-based stopping tolerances reached if self.opt['AutoRho', 'Enabled'] or not self.opt['FastSolve']: if r < epri and s < edua: break # Increment iteration count self.k += 1 # Record solve time self.timer.stop(['solve', 'solve_wo_func', 'solve_wo_rsdl']) # Print final separator string if Verbose option enabled self.display_end(nsep) self.Y = np.moveaxis(mp_Y1, mp_axisM, self.cri.axisM) self.X = np.moveaxis(mp_X, mp_axisM, self.cri.axisM) self.terminate_pool() return self.getmin() def init_pool(self): """Initialize multiprocessing pool if necessary.""" # initialize the pool if needed if self.pool is None: if self.nproc > 1: self.pool = mp.Pool(processes=self.nproc) else: self.pool = None else: print('pool already initialized?') def distribute(self, f, n): """Distribute the computations amongst the multiprocessing pools Parameters ---------- f : function Function to be distributed to the processors n : int The values in range(0,n) will be passed as arguments to the function f. """ if self.pool is None: return [f(i) for i in range(n)] else: return self.pool.map(f, range(n)) def terminate_pool(self): """Terminate and close the multiprocessing pool if necessary.""" if self.pool is not None: self.pool.terminate() self.pool.join() del (self.pool) self.pool = None def obfn_gvar(self): """Variable to be evaluated in computing :meth:`ADMM.obfn_g`, depending on the ``gEvalY`` option value. """ return mp_Y1 if self.opt['gEvalY'] else mp_X def obfn_fvar(self): """Variable to be evaluated in computing :meth:`ADMM.obfn_f`, depending on the ``fEvalX`` option value. """ return mp_X if self.opt['fEvalX'] else mp_Y1 def obfn_reg(self): r"""Compute regularisation term, :math:`\| x \|_1`, and contribution to objective function. """ l1 = np.sum(mp_wl1 * np.abs(self.obfn_gvar())) return (self.lmbda * l1, l1) def obfn_dfd(self): r"""Compute data fidelity term :math:`(1/2) \| W \left( \sum_m \mathbf{d}_m * \mathbf{x}_m - \mathbf{s} \right) \|_2^2`. """ XF = sl.rfftn(self.obfn_fvar(), mp_Nv, mp_axisN) DX = np.moveaxis( sl.irfftn(sl.inner(mp_Df, XF, mp_axisM), mp_Nv, mp_axisN), mp_axisM, self.cri.axisM) return np.sum((self.W * (DX - self.S))**2) / 2.0