Skip to content

Enable TV, L1-wavelet and other sparse regularisation in LSQR and CGLS, via a new IRLS algorithm and Tikhonov operators - #2369

Open
bryceshirley wants to merge 7 commits into
TomographicImaging:masterfrom
bryceshirley:pull-request-1-refactor
Open

bryceshirley wants to merge 7 commits into
TomographicImaging:masterfrom
bryceshirley:pull-request-1-refactor

Conversation

@bryceshirley

@bryceshirley bryceshirley commented Sep 7, 2026 •

Copy link
Copy Markdown

Description

Adds an IRLS (Iteratively Reweighted Least Squares) outer algorithm that solves the sparsity-promoting problem

$$\min_u |Au-b|_2^2 + \alpha^2|Lu|_1$$

as a sequence of reweighted least-squares problems, each handled by an inner LSQR or CGLS. To support it, LSQR and CGLS gain structured Tikhonov regularisation in block and standard form.

See the notebook regularisation-for-krylov-methods.ipynb‎ in CIL-Demos/misc pull request 292 for example usage.

Contribution Notes

  • The content of this Pull Request (the Contribution) is intentionally submitted for inclusion in CIL (the Work) under the terms and conditions of theApache-2.0 License
  • I confirm that the contribution does not violate any intellectual property rights of third parties

Checklist

  • I have performed a self-review of my code
  • I have added docstrings in line with the guidance in the developer guide
  • I have updated the relevant documentation
  • I have implemented unit tests that cover any new or modified functionality
  • CHANGELOG.md has been updated with any functionality change
  • Request review from all relevant developers

@bryceshirley
bryceshirley marked this pull request as draft September 7, 2026 18:42
@bryceshirley bryceshirley changed the title DRAFT: Enable TV, L1-wavelet and other sparse regularisation in LSQR and CGLS, via a new IRLS algorithm and Tikhonov operator family DRAFT: Enable TV, L1-wavelet and other sparse regularisation in LSQR and CGLS, via a new IRLS algorithm and Tikhonov operators Sep 7, 2026
@gfardell

gfardell commented Sep 9, 2026

Copy link
Copy Markdown
Member

This looks fantastic @bryceshirley, thank you! Would you be able to join a developer meeting to walk us through it (I think the 22nd is our next one)? I know you've already spoken to/worked with the team, but it would be good to go through the current status together.

@bryceshirley

Copy link
Copy Markdown
Author

This looks fantastic @bryceshirley, thank you! Would you be able to join a developer meeting to walk us through it (I think the 22nd is our next one)? I know you've already spoken to/worked with the team, but it would be good to go through the current status together.

Sounds good! I can present that pull request and explain the decisions in terms of interface and memory management, as well as alternatives. How much time should it be?

@MargaretDuff MargaretDuff self-assigned this Sep 16, 2026

@MargaretDuff MargaretDuff left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have left a bunch of comments, none of them very substantial. A lot of it was deleting unnecessary comments.

What I suggest, and would like to discuss at the developer meeting next week, is a change to the user interaction with IRLS. Currently, to use IRLS the user must pass a set up CGLS or LSQR algorithm, with the right operators, the right choice of the weighted flag in CGLS or LSQR. This confuses the standalone CGLS or LSQR algorithm with parameters that might not be relevant and makes things like the form parameter difficult to explain. It would also allow us to hide most of the new create_tikhonov_operator, resolve_form, WeightedStructOpeartor, BlockTikhonovOperator - they become implementation details rather than things a user must understand to call IRLS.

On the other hand, it looses some flexibility - it would be harder to pass a custom inner solver. Though possibly later on we could have a IRLS.from_solver(inner_solver,..) alternative set up later?

For IRLS it would allow us to have a safe default inner solver - LSQR is better conditions.

Comment thread CHANGELOG.md Resolved
Comment thread CHANGELOG.md Resolved

@MargaretDuff MargaretDuff left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have left a bunch of comments, none of them very substantial. A lot of it was deleting unnecessary comments.

What I suggest, and would like to discuss at the developer meeting next week, is a change to the user interaction with IRLS. Currently, to use IRLS the user must pass a set up CGLS or LSQR algorithm, with the right operators, the right choice of the weighted flag in CGLS or LSQR. This confuses the standalone CGLS or LSQR algorithm with parameters that might not be relevant and makes things like the form parameter difficult to explain. It would also allow us to hide most of the new create_tikhonov_operator, resolve_form, WeightedStructOpeartor, BlockTikhonovOperator - they become implementation details rather than things a user must understand to call IRLS.

On the other hand, it looses some flexibility - it would be harder to pass a custom inner solver. Though possibly later on we could have a IRLS.from_solver(inner_solver,..) alternative set up later?

For IRLS it would allow us to have a safe default inner solver - LSQR is better conditions.

Co-authored-by: Margaret Duff <43645617+MargaretDuff@users.noreply.github.com>
@bryceshirley
bryceshirley force-pushed the pull-request-1-refactor branch from 7fd6acb to aebf28e Compare October 13, 2026 19:28
@bryceshirley bryceshirley changed the title DRAFT: Enable TV, L1-wavelet and other sparse regularisation in LSQR and CGLS, via a new IRLS algorithm and Tikhonov operators Enable TV, L1-wavelet and other sparse regularisation in LSQR and CGLS, via a new IRLS algorithm and Tikhonov operators Oct 14, 2026
@bryceshirley
bryceshirley force-pushed the pull-request-1-refactor branch 2 times, most recently from 68e9097 to 172c647 Compare October 14, 2026 10:47
@bryceshirley
bryceshirley marked this pull request as ready for review October 14, 2026 10:50
@bryceshirley
bryceshirley force-pushed the pull-request-1-refactor branch from 172c647 to 050128e Compare October 14, 2026 10:58
@bryceshirley

bryceshirley commented Oct 14, 2026 •

Copy link
Copy Markdown
Author

I have updated the PR to now make IRLS a stand alone algorithm with default inner loop solver LSQR in block form and moved the demo notebook into a CIL-Demos pull request.

@MargaretDuff MargaretDuff left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick review of the CGLS algorithm. Will try and keep looking this afternoon

# Catch self.regalpha and the operator's regalpha disagreeing.
operator_alpha = getattr(self.operator, 'regalpha', None)
if operator_alpha is not None and operator_alpha != self.regalpha:
raise ValueError(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure a user would understand what this means if they see this error! When would you expect it to come up?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a lot of if block and if standard statements in this file. I wonder if instead we use a factory method? Could the user call CGLS and then inside that file it either sets up and returns classes _standardCGLS or _blockCGLS ? Or perhaps a standard CGLS that factories off to _blockCGLS when needed but that _blockCGLS also uses things from CGLS?

Realise we don't do this elsewhere in our algorithm class - @gfardell or @paskino would appreciate if you had any thoughts?

Co-authored-by: Margaret Duff <43645617+MargaretDuff@users.noreply.github.com>
Signed-off-by: Bryce Shirley <114733577+bryceshirley@users.noreply.github.com>

@MargaretDuff MargaretDuff left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A pass at IRLS - Thanks Bryce!


Allowing the inner solver to solve
.. math::
\min_x \|Ax - b\|_2^2 + \alpha^2 \|Lx\|_1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put this as the first equation on the algorithm? So,

  • This equation solves
  • It does this by
  • Any notes
  • Parameters

----------
initial : DataContainer, optional
Starting point of the first inner solve, and the point the first
weights are computed from. Defaults to zeros.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not a code question but for when we recommend this to users - are the first weights defaulted zeros? Would initialising the weights with a FBP recon be a good idea? If we put in bad initial weights, could the algorithm recover?

struct_operator : Operator, optional
The structural operator :math:`L`. ``None`` means the identity,
i.e. a plain L1 penalty on the solution.
inner_solver : class, default LSQR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you pass the class or pass a string? Could we pass both? I.e. a string either LSQR or CGLS or a class with these properties...

I think our basic users would prefer just a string but our mathematical users might appreciate being able to pass their own custom solver.


# Solution buffer for standard form's (WL)^-1 mapping; block form uses the live iterate.
self.tmp_solution = (
self.inner_solver.solution_geometry().allocate(0)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is probably not SIRF compatible

Comment on lines +227 to +231
u = self.inner_solver.get_output()

residual = self.inner_solver.operator.operator.direct(u)
residual.sapyb(1.0, self.inner_solver.data, -1.0, out=residual)
data_term = residual.squared_norm()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any way to reuse memory for this and not reallocate?

@MargaretDuff

Copy link
Copy Markdown
Member

@lauramurgatroyd @gfardell @paskino - Just to say that I ran out of time looking more at this. It would be great if you have time to give Bryce some feedback! Thank-you!

@bryceshirley
bryceshirley force-pushed the pull-request-1-refactor branch from 61b70d8 to e7f2821 Compare October 28, 2026 20:21

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Priority review

Development

Successfully merging this pull request may close these issues.

3 participants