Skip to content

(Gradually) add advanced interface to solvers #1400

@kohr-h

Description

@kohr-h

Copied from the discussion in #1252:

I am not sure that this change in API makes a big difference. It means that if one wants to have more specialised callbacks, then one can recycle some code by using the update function. However, in some applications one also might want to choose which values to compute and which ones to store. Thus, for these one needs to change the update function. Thus, I am not convinced one gains that much with the proposed changes.

If you want to write callbacks that do more interesting stuff, then yes, agreed. But the whole point of writing a class that exposes elements of the optimization method is to not have to use callbacks to do interesting stuff.
See, the purpose of callbacks is basically to get information out of a black box. You don't know if or when things will happen, but you say to the black box "Whenever you do X, please call this function." But if you have a "grey box" rather than a black box, you have all kinds of possibilities, depending on how much the box exposes.

So in our example, think of something like this:

Basic version

class Landweber(object):
    def __init__(self, op, x, rhs, omega=1):
        self.op = op
        self.x = x
        self.rhs = rhs
        self.omega = omega
        
    def step(self):
        self.x.assign(self.x - self.omega * self.op.adjoint(self.op(self.x) - self.rhs))
        
    def current(self):
        return self.x

More advanced version

class Landweber(object):
    def __init__(self, op, x, rhs, omega=1):
        self.op = op
        self.x = x
        self.rhs = rhs
        self.omega = omega
        
    def step(self):
        self.x.assign(self.current() - self.omega * self.current_l2sq_grad())

    def current_l2sq_grad(self):
        return self.op.adjoint(self.current_residual())

    def current_residual(self):
        return self.op(self.x) - self.rhs
        
    def current(self):
        return self.x

With intermediate storage

class Landweber(object):
    def __init__(self, op, x, rhs, omega=1, use_tmp_dom=True, use_tmp_ran=True):
        self.op = op
        self.x = x
        self.rhs = rhs
        self.omega = omega
        self.tmp_dom = self.op.domain.element() if use_tmp_dom else None
        self.tmp_ran = self.op.range.element() if use_tmp_ran else None
        
    def step(self):
        self.x.lincomb(1, self.x, -self.omega, self.current_l2sq_grad())

    def current_l2sq_grad(self):
        return self.op.adjoint(self.current_residual(), out=self.tmp_dom)

    def current_residual(self):
        tmp = self.op(self.x, out=self.tmp_ran)
        tmp -= self.rhs
        return tmp
        
    def current(self):
        return self.x

This now gives you all kinds of possibilities to alter the iteration, inspect intermediate results etc. without having to resort to callbacks.

Also note that these changes can be added gradually without changing the interface (step).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions