Source code for cil.optimisation.utilities.preconditioner

#  Copyright 2024 United Kingdom Research and Innovation
#  Copyright 2024 The University of Manchester
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
#
# Authors:
# - CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt


from abc import ABC, abstractmethod
import numpy as np


[docs] class Preconditioner(ABC): r""" Abstract base class for Preconditioner objects. The `apply` method of this class takes an initialised CIL function as an argument and modifies a provided `gradient`. Methods ------- apply(x) Abstract method to call the preconditioner. """ def __init__(self): '''Initialises the preconditioner ''' pass
[docs] @abstractmethod def apply(self, algorithm, gradient, out): r""" Abstract method to apply the preconditioner. Parameters ---------- algorithm : Algorithm The algorithm object. gradient : DataContainer The calculated gradient to modify out : DataContainer, Container to fill with the modified gradient. Note ----- In CIL algorithms, the preconditioners are used in-place. Make sure this method is safe to use in place. Returns ------- DataContainer The modified gradient """ pass
[docs] class Sensitivity(Preconditioner): r""" Sensitivity preconditioner class. In each call to the preconditioner the `gradient` is multiplied by :math:`1/(A^T \mathbf{1})` where :math:`A` is an operator, :math:`\mathbf{1}` is an object in the range of the operator filled with ones. Parameters ---------- operator : CIL Operator The operator used for sensitivity computation. """ def __init__(self, operator): super(Sensitivity, self).__init__() self.operator = operator self.compute_preconditioner_matrix()
[docs] def compute_preconditioner_matrix(self): r""" Compute the sensitivity. :math:`A^T \mathbf{1}` where :math:`A` is the operator and :math:`\mathbf{1}` is an object in the range of the operator filled with ones. Then perform safe division by the sensitivity to store the preconditioner array :math:`1/(A^T \mathbf{1})` """ self.array = self.operator.adjoint( self.operator.range_geometry().allocate(value=1.0)) try: self.operator.range_geometry().allocate(value=1.0).divide( self.array, where=np.abs(self.array.as_array()) > 0, out=self.array) except: # Due to CIL/SIRF compatibility and SIRF divide not taking kwargs sensitivity_np = self.array.as_array() self.pos_ind = np.abs(sensitivity_np) > 0 array_np = 0*sensitivity_np array_np[self.pos_ind] = (1./sensitivity_np[self.pos_ind]) self.array.fill(array_np)
[docs] def apply(self, algorithm, gradient, out=None): r""" Update the preconditioner. Parameters ---------- algorithm : object The algorithm object. gradient : DataContainer The calculated gradient to modify out : DataContainer, Container to fill with the modified gradient Returns ------- DataContainer The modified gradient """ if out is None: out = gradient.copy() gradient.multiply( self.array, out=out) return out
[docs] class AdaptiveSensitivity(Sensitivity): r""" Adaptive Sensitivity preconditioner class. In each call to the preconditioner the `gradient` is multiplied by :math:`(x+\delta) /(A^T \mathbf{1})` where :math:`A` is an operator, :math:`\mathbf{1}` is an object in the range of the operator filled with ones. The point :math:`x` is the current iteration, or a reference image, and :math:`\delta` is a small positive float. Parameters ---------- operator : CIL object The operator used for sensitivity computation. delta : float, optional The delta value for the preconditioner. reference : DataContainer e.g. ImageData, default is None Reference data, an object in the domain of the operator. Recommended to be a best guess reconstruction. If reference data is passed the preconditioner is always fixed. max_iterations : int, default = 100 The maximum number of iterations before the preconditoner is frozen and no-longer updates. Note that if reference data is passed the preconditioner is always frozen and `iterations` is set to -1. Note ---- A reference for the freezing of the preconditioner can be found: Twyman R., Arridge S., Kereta Z., Jin B., Brusaferri L., Ahn S., Stearns CW., Hutton B.F., Burger I.A., Kotasidis F., Thielemans K.. An Investigation of Stochastic Variance Reduction Algorithms for Relative Difference Penalized 3D PET Image Reconstruction. IEEE Trans Med Imaging. 2023 Jan;42(1):29-41. doi: 10.1109/TMI.2022.3203237. Epub 2022 Dec 29. PMID: 36044488. """ def __init__(self, operator, delta=1e-6, max_iterations=100, reference=None): self.max_iterations = max_iterations self.delta = delta super(AdaptiveSensitivity, self).__init__( operator=operator) self.freezing_point = operator.domain_geometry().allocate(0) if reference is not None: reference += self.delta self.array.multiply(reference, out=self.freezing_point) reference -= self.delta self.max_iterations = -1
[docs] def apply(self, algorithm, gradient, out=None): r""" Update the preconditioner. Parameters ---------- algorithm : object The algorithm object. gradient : DataContainer The calculated gradient to modify out : DataContainer, Container to fill with the modified gradient Returns ------- DataContainer The modified gradient """ if out is None: out = gradient.copy() if algorithm.iteration <= self.max_iterations: self.freezing_point.fill(algorithm.solution) self.freezing_point.add(self.delta, out=self.freezing_point) self.array.multiply(self.freezing_point, out=self.freezing_point) gradient.multiply( self.freezing_point, out=out) else: gradient.multiply( self.freezing_point, out=out) return out