Skip to content

Commit 5d46550

Browse files
lauramurgatroydMargaretDuffgfardell
authored
Remove code which has been deprecated since v24.3 or earlier (#2309)
* Remove deprecated code * fix test failures * handle max iterations * Fix progress test * Fix some GD issues * Fix callbacks fail * Fix issue with CGLS iterating after fully converged on exact solution * Add to Change log * Margaret's first pass, fixing failing test and deprecating a couple more things * White space changes * Added rosen with fixed stepsize test back * Update documentation --------- Signed-off-by: Laura Murgatroyd <60604372+lauramurgatroyd@users.noreply.github.com> Co-authored-by: Margaret Duff <margaret.duff@stfc.ac.uk> Co-authored-by: Gemma Fardell <47746591+gfardell@users.noreply.github.com> Co-authored-by: gfardell <gemma.fardell@stfc.ac.uk>
1 parent 639e5fe commit 5d46550

19 files changed

Lines changed: 472 additions & 736 deletions

CHANGELOG.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,27 @@
2222
- Update to ASTRA-TOOLBOX version v2.4 from the `astra-toolbox` channel (#2330)
2323
- Update to TIGRE v3.1.3 (#2317)
2424
- Added support for numpy 2 (#2218)
25-
- Update to CCPi-Regularisation toolkit 26.0.0
26-
- Added support for python 3.13
25+
- Added utililty method `dtype_like` to convert an input value to the same type as a reference array
26+
- Added documentation to developer guide on scalar types with numpy>=2.
27+
- Update to CCPi-Regularisation toolkit v26.0.0 (#2218)
28+
- Update to Tomophantom v3.1.4 (#2218)
29+
- Added support for python 3.13 (#2218)
2730
- Documentation:
2831
- Render the user showcase notebooks in the documentation (#2189)
2932
- Update on build instructions in README and developer guide for all OS (#2286)
3033
- Enhancements:
3134
- Add prefix argument to TIFFStackReader to load a subset of TIFF files in
3235
a folder (#2239)
3336
- Update ASTRA interface to `direct_FP3D/BP3D` removing copies for GPU `ProjectionOperator` calls (#2134)
37+
- Removes the following code which had been deprecated since v24.3.0 or earlier:
38+
- Removes `max_iteration` and `log_file` input parameters to `Algorithm`s.
39+
- Removes `max_iteration_stop_criterion`, `objective_to_string`, `verbose_output` and `verbose_header` methods from `Algorithm`s.
40+
- Removes `print_interval` and `callback` kwargs from `Algorithm.run` (note: `callbacks` kwarg remains)
41+
- Removes `tolerance` input parameter to `CGLS`.
42+
- Removes `should_stop` and `flag` methods from `CGLS`.
43+
- Removes `alpha`, `beta`, `rtol` and `atol` kwargs from `GD`.
44+
- Removes `should_stop` and `objective_function` methods from `GD`.
45+
- Removes `norms` and `prob` kwargs from `SPDHG`.
3446
- Testing:
3547
- migrate from `conda build` to `rattler-build`
3648

Wrappers/Python/cil/optimisation/algorithms/APGD.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ class ScalarMomentumCoefficient(ABC):
3434
The call method of the ScalarMomentumCoefficient returns a scalar value. Given access to the algorithm object, the momentum coefficient can be a function of the algorithm state.
3535
3636
The `apply_momentum_in_APGD` function, updates the solution in the APGD algorithm as
37-
37+
3838
.. math:: y_{k+1}=x_{k+1}+M(x_{k+1}-x_{k}),
39-
39+
4040
where :math:`M` is the calculated scalar momentum value.
4141
4242
@@ -61,7 +61,7 @@ def __call__(self, algorithm):
6161

6262
def apply_momentum_in_APGD(self, algorithm, out=None):
6363
r'''Calculates the momentum cofficient, applies a scalar momentum update in the APGD algorithm and returns the next iterate.
64-
64+
6565
Uses the calculation, :math:`y_{k+1}=x_{k+1}+M(x_{k+1}-x_{k})`, where :math:`M` is the calculated scalar momentum value.
6666
6767
Parameters
@@ -98,7 +98,7 @@ class NesterovMomentum(ScalarMomentumCoefficient):
9898
Starting with :math:`t=1`, the Nesterov algorithm updates with each iteration:
9999
100100
.. math:: t_{k+1}=\dfrac{1}{2}(1+\sqrt{1+4t_{k}^2})
101-
101+
102102
The momentum coefficient is then returned as :math:`\dfrac{t_{k}-1}{t_{k}}`.
103103
'''
104104

@@ -126,7 +126,7 @@ class APGD(Algorithm):
126126
where :math:`\alpha` is the :code:`step_size`.
127127
128128
Then, :math:`y_{k+1}`, is then calculated from :math:`x_{k+1}`, based on a momentum rule. Note that :math:`y_0=x_0`. Users have flexibility to do this however they wish by passing to `momentum` a class that has an `apply_montemum_in_APGD` function which takes an intialised algorithm and returns the next iterate.
129-
129+
130130
131131
Currently, we have implemented options for a scalar momentum coefficient (see :class:`cil.optimisation.algorithms.APGD.ScalarMomentumCoefficient` class.). In this case, the momentum term is added as follows:
132132
@@ -233,9 +233,9 @@ def update(self):
233233
r"""Performs a single iteration of APGD. For :math:`k\geq 1`:
234234
235235
.. math::
236-
236+
237237
x_{k+1} = \mathrm{prox}_{\alpha g}(y_{k} - \alpha\nabla f(y_{k}))\\
238-
238+
239239
240240
where :math:`\alpha` is the step size. From :math:`x_{k+1}` (and any other information available in the algorithm class) the momentum function then calculates :math:`y_{k+1}`.
241241
"""
@@ -293,14 +293,15 @@ def step_size(self):
293293
'''
294294
Returns the most recently used step size. Note, if the step-size is set by a non-constant step size rule, you must use the algorithm run or update method before this getter will return the most recently used step size.
295295
'''
296-
296+
297297
if isinstance(self.step_size_rule, ConstantStepSize):
298298
return self.step_size_rule.step_size
299299
else:
300-
try:
300+
try:
301301
return self._step_size
302302
except AttributeError:
303-
raise NotImplementedError("Note the step-size is set by a step-size rule and could change with each iteration. Call the algorithm run or update method first and then this function will give the most recently used step size.")
303+
raise NotImplementedError(
304+
"Note the step-size is set by a step-size rule and could change with each iteration. Call the algorithm run or update method first and then this function will give the most recently used step size.")
304305

305306
@property
306307
def momentum(self):

Wrappers/Python/cil/optimisation/algorithms/Algorithm.py

Lines changed: 40 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -22,65 +22,43 @@
2222

2323
import numpy as np
2424

25-
from cil.optimisation.utilities.callbacks import Callback, LogfileCallback, _OldCallback, ProgressCallback
25+
from cil.optimisation.utilities.callbacks import Callback, ProgressCallback
2626

2727

2828
class Algorithm:
2929
r"""Base class providing minimal infrastructure for iterative algorithms.
3030
3131
An iterative algorithm is designed to solve an optimization problem by repeatedly refining a solution. In CIL, we use iterative algorithms to minimize an objective function, often referred to as a loss. The process begins with an initial guess, and with each iteration, the algorithm updates the current solution based on the results of previous iterations (previous iterates). Iterative algorithms typically continue until a stopping criterion is met, indicating that an optimal or sufficiently good solution has been found. In CIL, stopping criteria can be implemented using a callback function (`cil.optimisation.utilities.callbacks`).
32-
32+
3333
The user is required to implement the :code:`set_up`, :code:`__init__`, :code:`update` and :code:`update_objective` methods.
3434
3535
The method :code:`run` is available to run :code:`n` iterations. The method accepts :code:`callbacks`: a list of callables, each of which receive the current Algorithm object (which in turn contains the iteration number and the actual objective value) and can be used to trigger print to screens and other user interactions. The :code:`run` method will stop when the stopping criterion is met or `StopIteration` is raised.
36-
36+
3737
Parameters
3838
----------
3939
update_objective_interval: int, optional, default 1
4040
The objective (or loss) is calculated and saved every `update_objective_interval`. 1 means every iteration, 2 every 2 iterations and so forth. This is by default 1 and should be increased when evaluating the objective is computationally expensive.
4141
"""
4242

43-
def __init__(self, update_objective_interval=1, max_iteration=None, log_file=None):
43+
def __init__(self, update_objective_interval=1):
4444

4545
self.iteration = -1
46-
self.__max_iteration = 1
47-
if max_iteration is not None:
48-
warn("use `Algorithm.run(iterations)` instead of `Algorithm(max_iteration)`", DeprecationWarning, stacklevel=2)
49-
self.__max_iteration = max_iteration
46+
self._total_iterations = 1
5047
self.__loss = []
5148
self.memopt = False
5249
self.configured = False
5350
self._iteration = []
5451
self.update_objective_interval = update_objective_interval
55-
# self.x = None
5652
self.iter_string = 'Iter'
57-
if log_file is not None:
58-
warn("use `run(callbacks=[LogfileCallback(log_file)])` instead of `log_file`",
59-
DeprecationWarning, stacklevel=2)
60-
self.__log_file = log_file
6153

6254
def set_up(self, *args, **kwargs):
6355
'''Set up the algorithm'''
6456
raise NotImplementedError
57+
6558
def update(self):
6659
'''A single iteration of the algorithm'''
6760
raise NotImplementedError
6861

69-
def should_stop(self):
70-
'''default stopping criterion: number of iterations
71-
72-
The user can change this in concrete implementation of iterative algorithms.'''
73-
return self.iteration > self.max_iteration
74-
75-
def __set_up_logger(self, *_, **__):
76-
"""Do not use: this is being deprecated"""
77-
warn("use `run(callbacks=[LogfileCallback(log_file)])` instead", DeprecationWarning, stacklevel=2)
78-
79-
def max_iteration_stop_criterion(self):
80-
"""Do not use: this is being deprecated"""
81-
warn("use `should_stop()` instead of `max_iteration_stop_criterion()`", DeprecationWarning, stacklevel=2)
82-
return self.iteration > self.max_iteration
83-
8462
def __iter__(self):
8563
'''Algorithm is an iterable'''
8664
return self
@@ -90,22 +68,21 @@ def __next__(self):
9068
9169
This method triggers :code:`update()` and :code:`update_objective()`
9270
'''
93-
if self.should_stop():
94-
raise StopIteration
9571
if self.iteration == -1 and self.update_objective_interval > 0:
9672
self._iteration.append(self.iteration)
9773
self.update_objective()
9874
self.iteration += 1
9975
return self.iteration
10076
if not self.configured:
101-
raise ValueError('Algorithm not configured correctly. Please run set_up.')
77+
raise ValueError(
78+
'Algorithm not configured correctly. Please run set_up.')
10279
self.update()
10380
self.iteration += 1
10481

10582
self._update_previous_solution()
10683

10784
if self.iteration >= 0 and self.update_objective_interval > 0 and\
108-
self.iteration % self.update_objective_interval == 0:
85+
self.iteration % self.update_objective_interval == 0:
10986

11087
self._iteration.append(self.iteration)
11188
self.update_objective()
@@ -127,32 +104,33 @@ def _update_previous_solution(self):
127104

128105
def get_output(self):
129106
r""" Returns the current solution.
130-
107+
131108
Returns
132109
-------
133110
DataContainer
134111
The current solution
135-
112+
136113
"""
137114
return self.x
138115

139116
def _provable_convergence_condition(self):
140117
r""" Checks if the algorithm set-up (e.g. chosen step-sizes or other parameters) meets a mathematical convergence criterion.
141-
118+
142119
Returns
143120
-------
144121
bool: Outcome of the convergence check
145122
"""
146-
raise NotImplementedError(" Convergence criterion is not implemented for this algorithm. ")
123+
raise NotImplementedError(
124+
" Convergence criterion is not implemented for this algorithm. ")
147125

148126
def is_provably_convergent(self):
149127
r""" Check if the algorithm is convergent based on the provable convergence criterion.
150-
128+
151129
Returns
152130
-------
153131
Boolean
154132
Outcome of the convergence check
155-
133+
156134
"""
157135
return self._provable_convergence_condition()
158136

@@ -163,17 +141,17 @@ def solution(self):
163141

164142
def get_last_loss(self, return_all=False):
165143
r'''Returns the last stored value of the loss function. "Loss" is an alias for "objective value". If `update_objective_interval` is 1 it is the value of the objective at the current iteration. If update_objective_interval > 1 it is the last stored value.
166-
144+
167145
Parameters
168146
----------
169147
return_all: Boolean, default is False
170148
If True, returns all the stored loss functions
171-
149+
172150
Returns
173151
-------
174152
Float
175153
Last stored value of the loss function
176-
154+
177155
'''
178156
try:
179157
objective = self.__loss[-1]
@@ -183,7 +161,7 @@ def get_last_loss(self, return_all=False):
183161
return objective if return_all else objective[0]
184162
return [objective, np.nan, np.nan] if return_all else objective
185163

186-
get_last_objective = get_last_loss # alias
164+
get_last_objective = get_last_loss # alias
187165

188166
def update_objective(self):
189167
'''calculates the objective with the current solution'''
@@ -193,7 +171,7 @@ def update_objective(self):
193171
def iterations(self):
194172
'''returns the iterations at which the objective has been evaluated'''
195173
return self._iteration
196-
174+
197175
@property
198176
def loss(self):
199177
'''returns a list of the values of the objective (alias of loss) during the iteration
@@ -202,18 +180,7 @@ def loss(self):
202180
'''
203181
return self.__loss
204182

205-
objective = loss # alias
206-
207-
@property
208-
def max_iteration(self):
209-
'''gets the maximum number of iterations'''
210-
return self.__max_iteration
211-
212-
@max_iteration.setter
213-
def max_iteration(self, value):
214-
'''sets the maximum number of iterations'''
215-
assert isinstance(value, Integral) or np.isposinf(value)
216-
self.__max_iteration = value
183+
objective = loss # alias
217184

218185
@property
219186
def update_objective_interval(self):
@@ -227,9 +194,9 @@ def update_objective_interval(self, value):
227194
raise ValueError('interval must be an integer >= 0')
228195
self.__update_objective_interval = value
229196

230-
def run(self, iterations=None, callbacks: Optional[List[Callback]]=None, verbose=1, **kwargs):
197+
def run(self, iterations=None, callbacks: Optional[List[Callback]] = None, verbose=1):
231198
r"""run upto :code:`iterations` with callbacks/logging.
232-
199+
233200
For a demonstration of callbacks see https://github.com/TomographicImaging/CIL-Demos/blob/main/misc/callback_demonstration.ipynb
234201
235202
Parameters
@@ -241,41 +208,29 @@ def run(self, iterations=None, callbacks: Optional[List[Callback]]=None, verbose
241208
verbose: 0=quiet, 1=info, 2=debug
242209
Passed to the default callback to determine the verbosity of the printed output.
243210
"""
244-
211+
245212
if iterations is None:
246213
raise ValueError("`run()` missing number of `iterations`")
247-
248-
if 'print_interval' in kwargs:
249-
warn("use `TextProgressCallback(miniters)` instead of `run(print_interval)`",
250-
DeprecationWarning, stacklevel=2)
214+
251215
if np.isposinf(iterations):
252216
if callbacks is None:
253-
raise ValueError("Infinite iterations require a callback with a stopping criterion that raises `StopIteration`")
217+
raise ValueError(
218+
"Infinite iterations require a callback with a stopping criterion that raises `StopIteration`")
254219
else:
255-
warn("Infinite iterations require a callback with a stopping criterion that raises `StopIteration`", UserWarning, stacklevel=2)
256-
220+
warn("Infinite iterations require a callback with a stopping criterion that raises `StopIteration`",
221+
UserWarning, stacklevel=2)
222+
257223
if callbacks is None:
258224
callbacks = [ProgressCallback(verbose=verbose)]
259-
260-
261-
# transform old-style callbacks into new
262-
callback = kwargs.get('callback', None)
263-
if callback is not None:
264-
callbacks.append(_OldCallback(callback, verbose=verbose))
265-
if hasattr(self, '__log_file'):
266-
callbacks.append(LogfileCallback(self.__log_file, verbose=verbose))
267-
268-
if self.should_stop():
269-
print("Stop criterion has been reached.")
270-
271-
272-
if self.iteration == -1 and self.update_objective_interval>0:
273-
iterations+=1
274-
275-
# call `__next__` upto `iterations` times or until `StopIteration` is raised
276-
self.max_iteration = self.iteration + iterations
277-
iters = (count(self.iteration) if np.isposinf(self.max_iteration)
278-
else range(self.iteration, self.max_iteration))
225+
226+
if self.iteration == -1 and self.update_objective_interval > 0:
227+
iterations += 1
228+
229+
self._total_iterations = self.iteration + iterations
230+
231+
# call `__next__` up to `iterations` times or until `StopIteration` is raised
232+
iters = (count(self.iteration) if np.isposinf(self._total_iterations)
233+
else range(self.iteration, self._total_iterations))
279234
for _ in iters:
280235
try:
281236
self.__next__()
@@ -292,16 +247,3 @@ def objective_to_dict(self, verbose=False):
292247
return {'primal': obj[0], 'dual': obj[1], 'primal_dual': obj[2]}
293248
obj = obj[0]
294249
return {'objective': obj}
295-
296-
def objective_to_string(self, verbose=False):
297-
"""Do not use: this is being deprecated"""
298-
warn("consider using `run(callbacks=[LogfileCallback(log_file)])` instead", DeprecationWarning, stacklevel=2)
299-
return str(self.objective_to_dict(verbose=verbose))
300-
301-
def verbose_output(self, *_, **__):
302-
"""Do not use: this is being deprecated"""
303-
warn("use `run(callbacks=[ProgressCallback()])` instead", DeprecationWarning, stacklevel=2)
304-
305-
def verbose_header(self, *_, **__):
306-
"""Do not use: this is being deprecated"""
307-
warn("consider using `run(callbacks=[LogfileCallback(log_file)])` instead", DeprecationWarning, stacklevel=2)

0 commit comments

Comments
 (0)