artofscience / SAOR

Sequential Approximate Optimization Repository
GNU General Public License v3.0
5 stars 1 forks source link

Add the abstract `Problem` class #12

Closed MaxvdKolk closed 3 years ago

MaxvdKolk commented 3 years ago

The discussions seems to converge towards the solution where we approach a single Problem class for this moment. This class then could hold various functions for the api

class Problem:
    def __init__(self, ...)
    def response(self, x)
    def sensitivity(self, x)
    def sensitivty2(self, x)

This is similar to the other Response class that was discussed, and as @aatmdelissen mentioned, we could get away with using subclasses of either Problem or Response rather then defining them both.

Personally still doubt on function names: response, sensitivity. These are a bit verbose, might be nice for reading the code, but it could be that

class Problem
    def __init__(self)
    def g(self, x)
    def dg(self, x)
    def ddg(self, x)

is sufficient in the context. The actual naming might require some further discussion.


Note: the current code in dev mostly defines each problem simply as a class, e.g. class Li2015Fig4 as a base object. I would propose to still define the abstract interface for a Problem (with the above API) and then make this subclassing more explicity, e.g.

class Li2015Fig4(Problem)
class Rosenbrock(Problem)

This helps documentation purposes, but also makes it clear for any user how to define problems using a Problem class.

artofscience commented 3 years ago

Imo g, dg, ddg is self-explanatory. However, it might be confusing as this is also used for the "sub"-problem optimizer. I would still op for g, dg, and ddg. Also, because you could also use the "sub"problem solvers to directly solve a simple Problem (if the problem is convex separable).

artofscience commented 3 years ago

Wrt your note: myproblem(Problem) sounds perfect.

Note. we might want to store g in the Problem class for certain problems that require x (or any other property) to calculate the sensitivities. E.g. store displacements u in the problem class. Maybe make a child of Problem that has this functionality?

Giannis1993 commented 3 years ago

Agreed! Lets keep the following format then:

class Problem
    def __init__(self)
    def g(self, x)
    def dg(self, x)
    def ddg(self, x)
class Li2015Fig4(Problem)
class Rosenbrock(Problem)

I'll pull the new dev branch, make a new branch dev-PROBLEM_STRUCTURE, edit these things, push it, and make a pull request for that so we can close the issue after merging it into dev.


@artofscience why do we need a separate child class for the problems that need certain attributes (e.g. prob.u) to compute their sensitivities? Since this occurs only in certain problems, can we not simply add prob.u to the problems that need it?

MaxvdKolk commented 3 years ago

Maybe @artofscience means that if there is a specific pattern for certain problems, for example to store/cache the solution of the simulation, we could provide a class that does something like that. Such that a user can implement only one function, that automatically deals with storing the solution/sensitivity as attribute and then returns whatever is needed when called through g, dg, ddg

class CachedProblem(Problem):
    def __init__(self, ...):
        # cached response/sensitivity 
        self.u, self.du = (None, None)

    def simulation(self, x):
        # perform simulation
        # this could also include a flag to indicate to simulation if it should
        # or should not evaluate the sensitivity/backward solutions
        self.u, self.du = solve_external_simulation(x)

    def g(self, x):
        # renew the simulation for `g`?
        self.simulation(x)
        return self.u

    def dg(self, x):
        # only renew simulation when sensitivity not present
        # otherwise just return whatever is present in the cache
        if not self.du:
            self.simulation(x)
        return self.du

Not sure if @artofscience hinted at this, but in this case we could make only simulation an abstract interface where the user needs to provide its call to solve the response and/or sensitivity based on a set of design variables

For an external user it would be

from sao.problems import CachedProblem

class MyCalculixProblem(CachedProblem):
    def simulation(self, x):
        # wrapper code goes here
aatmdelissen commented 3 years ago

What is the exact purpose of the ddg method, if no explicit second derivatives are calculated? Does the abstract base-class provide default options for ddg? BFGS-kind of approximations?

The option @MaxvdKolk (CachedProblem) might be nice as a simple extension indeed, wrapping function-oriented code.

For the multiple-responses code, we might use something like

class MultiProblem(Problem):
    def __init__(self):
        self.problem[0] = Objective()
        self.problem[1] = Constraint()
        ...

    def g(self, x):
        u[:,0] = self.problem[0].g(x)
        u[:,1] = self.problem[1].g(x)
        ...
        return u
Giannis1993 commented 3 years ago

@aatmdelissen There is no reason for ddg if it is not implemented, that's why it is not decorated with @abstractmethod in the class Problem(ABC). At a later stage however, we could indeed opt to place some BFGS-like scheme as a default option, if a 2nd-order Taylor expansion is chosen.

Giannis1993 commented 3 years ago

This issue continues as an enhancement at #28