Ejemplo n.º 1
0
    def setup(self):
        """ Sets up nonlinear optimization machinery
        """
        # prepare output writers
        self.writer = Writer(path=PATH.WORKDIR + '/' + 'output.stats')

        self.stepwriter = StepWriter(path=PATH.WORKDIR + '/' + 'output.optim')

        unix.mkdir(PATH.OPTIMIZE)

        # write initial model
        if 'MODEL_INIT' in PATH:
            solver = sys.modules['seisflows_solver']
            self.save('m_new', solver.merge(solver.load(PATH.MODEL_INIT)))
Ejemplo n.º 2
0
class base(object):
    """ Abstract base class

     Default numerical parameters provided below should work well for a wide range
     inversions without the need for manual tuning. If the nonlinear optimization
     procedure stagnates, it may be due to the objective function rather than the 
     numerical parameters.

     To reduce memory overhead, vectors are read from disk rather than passed
     from a calling routine. At the start of each search direction computation
     the current model and gradient are read from files 'm_new' and 'g_new';
     the resulting search direction is written to 'p_new'. As the inversion
     progresses, other information is stored to disk as well.
    """
    def check(self):
        """ Checks parameters, paths, and dependencies
        """
        # line search algorithm
        if 'LINESEARCH' not in PAR:
            setattr(PAR, 'LINESEARCH', 'Bracket')

        # preconditioner
        if 'PRECOND' not in PAR:
            setattr(PAR, 'PRECOND', None)

        # maximum number of trial steps
        if 'STEPMAX' not in PAR:
            setattr(PAR, 'STEPMAX', 10)

        # initial step length as percent of current model
        if 'STEPINIT' not in PAR:
            setattr(PAR, 'STEPINIT', 0.05)

        # optional initial step length safegaurd
        if 'STEPTHRESH' not in PAR:
            setattr(PAR, 'STEPTHRESH', None)

        # step length factor in bracketing line search
        if 'STEPFACTOR' not in PAR:
            setattr(PAR, 'STEPFACTOR', 0.5)

        # optional parameter, useful for NLCG line search
        if 'STEPOVERSHOOT' not in PAR:
            setattr(PAR, 'STEPOVERSHOOT', 0.)

        # ad hoc factor by which to scale gradient
        if 'ADHOCFACTOR' not in PAR:
            setattr(PAR, 'ADHOCFACTOR', 1.)

        # where temporary files are written
        if 'OPTIMIZE' not in PATH:
            setattr(PATH, 'OPTIMIZE', PATH.SCRATCH + '/' + 'optimize')

        # assertions
        if 'WORKDIR' not in PATH:
            raise ParameterError

        if PAR.OPTIMIZE in ['base']:
            print msg.CompatibilityError1
            sys.exit(-1)

        if PAR.PRECOND:
            assert PAR.PRECOND in dir(preconds)

    def setup(self):
        """ Sets up nonlinear optimization machinery
        """
        # prepare output writers
        self.writer = Writer(path=PATH.WORKDIR + '/' + 'output.stats')

        self.stepwriter = StepWriter(path=PATH.WORKDIR + '/' + 'output.optim')

        unix.mkdir(PATH.OPTIMIZE)

        # write initial model
        if 'MODEL_INIT' in PATH:
            solver = sys.modules['seisflows_solver']
            self.save('m_new', solver.merge(solver.load(PATH.MODEL_INIT)))

    def precond(self):
        """ Loads preconditioner machinery
        """
        if PAR.PRECOND in dir(preconds):
            return getattr(preconds, PAR.PRECOND)()
        else:
            return None

    # The following names are used in the 'compute_direction' method and for
    # writing information to disk:
    #    m_new - current model
    #    m_old - previous model
    #    m_try - trial model
    #    f_new - current objective function value
    #    f_old - previous objective function value
    #    f_try - trial objective function value
    #    g_new - current gradient direction
    #    g_old - previous gradient direction
    #    p_new - current search direction
    #    p_old - previous search direction
    #    s_new - current slope along search direction
    #    s_old - previous slope along search direction
    #    alpha - trial step length

    def compute_direction(self):
        """ Computes model update direction from stored gradient
        """
        # must be implemented by subclass
        raise NotImplementedError

    # The following names are used exclusively for the line search:
    #     m - model vector
    #     p - search direction vector
    #     s - slope along search direction
    #     f - value of objective function, evaluated at m
    #     x - step length along search direction
    #     p_ratio - ratio of model norm to search direction norm
    #     s_ratio - ratio of current slope to previous slope

    def initialize_search(self):
        """ Determines initial step length for line search
        """
        m = self.load('m_new')
        p = self.load('p_new')
        f = self.loadtxt('f_new')
        norm_m = max(abs(m))
        norm_p = max(abs(p))
        p_ratio = float(norm_m / norm_p)

        # reset search history
        self.search_history = [[0., f]]
        self.step_count = 0
        self.isdone = 0
        self.isbest = 0
        self.isbrak = 0

        # determine initial step length
        if self.iter == 1:
            alpha = p_ratio * PAR.STEPINIT
        elif self.restarted:
            alpha = p_ratio * PAR.STEPINIT
        elif PAR.OPTIMIZE in ['LBFGS']:
            alpha = 1.
        else:
            alpha = self.initial_step()

        # optional ad hoc scaling
        if PAR.STEPOVERSHOOT:
            alpha *= PAR.STEPOVERSHOOT

        # optional maximum step length safegaurd
        if PAR.STEPTHRESH:
            if alpha > p_ratio * PAR.STEPTHRESH and \
                self.iter > 1:
                alpha = p_ratio * PAR.STEPTHRESH

        # write trial model corresponding to chosen step length
        self.savetxt('alpha', alpha)
        self.save('m_try', m + alpha * p)

        # append latest statistics
        self.stepwriter(steplen=0., funcval=f)

    def update_status(self):
        """ Updates line search status

            Maintains line search history by keeping track of step length and
            function value from each trial model evaluation. From line search
            history, determines whether stopping criteria have been satisfied.
        """
        x_ = self.loadtxt('alpha')
        f_ = self.loadtxt('f_try')
        if np.isnan(f_):
            raise ValueError

        # update search history
        self.search_history += [[x_, f_]]
        self.step_count += 1
        x = self.step_lens()
        f = self.func_vals()

        fmin = f.min()
        imin = f.argmin()

        # is current step length the best so far?
        vals = self.func_vals(sort=False)
        if np.all(vals[-1] < vals[:-1]):
            self.isbest = 1

        # are stopping criteria satisfied?
        if PAR.LINESEARCH == 'Fixed':
            if (fmin < f[0]) and any(fmin < f[imin:]):
                self.isdone = 1

        elif PAR.LINESEARCH == 'Bracket' or \
            self.iter == 1 or self.restarted:
            if self.isbrak:
                self.isdone = 1
            elif (fmin < f[0]) and any(fmin < f[imin:]):
                self.isbrak = 1

        elif PAR.LINESEARCH == 'Backtrack':
            if fmin < f[0]:
                self.isdone = 1

        # append latest statistics
        self.stepwriter(steplen=x_, funcval=f_)

        return self.isdone

    def compute_step(self):
        """ Computes next trial step length
        """
        m = self.load('m_new')
        g = self.load('g_new')
        p = self.load('p_new')
        s = self.loadtxt('s_new')

        norm_m = max(abs(m))
        norm_p = max(abs(p))
        p_ratio = float(norm_m / norm_p)

        x = self.step_lens()
        f = self.func_vals()

        # compute trial step length
        if PAR.LINESEARCH == 'Fixed':
            alpha = p_ratio * (self.step_count + 1) * PAR.STEPINIT

        elif PAR.LINESEARCH == 'Bracket' or \
            self.iter==1 or self.restarted:
            if any(f[1:] < f[0]) and (f[-2] < f[-1]):
                alpha = polyfit2(x, f)

            elif any(f[1:] <= f[0]):
                alpha = self.loadtxt('alpha') * PAR.STEPFACTOR**-1
            else:
                alpha = self.loadtxt('alpha') * PAR.STEPFACTOR

        elif PAR.LINESEARCH == 'Backtrack':
            # calculate slope along 1D profile
            slope = s / self.dot(g, g)**0.5
            if PAR.ADHOCFACTOR:
                slope *= PAR.ADHOCFACTOR

            alpha = backtrack2(f[0], slope, x[1], f[1], b1=0.1, b2=0.5)

        # write trial model corresponding to chosen step length
        self.savetxt('alpha', alpha)
        self.save('m_try', m + alpha * p)

    def finalize_search(self):
        """ Cleans working directory and writes updated model
        """
        m = self.load('m_new')
        g = self.load('g_new')
        p = self.load('p_new')
        s = self.loadtxt('s_new')

        x = self.step_lens()
        f = self.func_vals()

        # clean working directory
        unix.cd(PATH.OPTIMIZE)
        unix.rm('alpha')
        unix.rm('m_try')
        unix.rm('f_try')

        if self.iter > 1:
            unix.rm('m_old')
            unix.rm('f_old')
            unix.rm('g_old')
            unix.rm('p_old')
            unix.rm('s_old')

        unix.mv('m_new', 'm_old')
        unix.mv('f_new', 'f_old')
        unix.mv('g_new', 'g_old')
        unix.mv('p_new', 'p_old')
        unix.mv('s_new', 's_old')

        # write updated model
        alpha = x[f.argmin()]
        savetxt('alpha', alpha)
        self.save('m_new', m + alpha * p)
        savetxt('f_new', f.min())

        # append latest statistics
        self.writer('factor',
                    -self.dot(g, g)**-0.5 * (f[1] - f[0]) / (x[1] - x[0]))
        self.writer('gradient_norm_L1', np.linalg.norm(g, 1))
        self.writer('gradient_norm_L2', np.linalg.norm(g, 2))
        self.writer('misfit', f[0])
        self.writer('restarted', self.restarted)
        self.writer('slope', (f[1] - f[0]) / (x[1] - x[0]))
        self.writer('step_count', self.step_count)
        self.writer('step_length', x[f.argmin()])
        self.writer('theta', 180. * np.pi**-1 * angle(p, -g))

        self.stepwriter.newline()

    def retry_status(self):
        """ Returns false if search direction was the same as gradient
          direction; returns true otherwise
        """
        g = self.load('g_new')
        p = self.load('p_new')

        thresh = 1.e-3
        theta = angle(p, -g)

        if PAR.VERBOSE >= 2:
            print ' theta: %6.3f' % theta

        if abs(theta) < thresh:
            return 0
        else:
            return 1

    def restart(self):
        """ Discards history of algorithm; prepares to start again from 
          gradient direction
        """
        g = self.load('g_new')
        self.save('p_new', -g)
        savetxt('s_new', self.dot(g, g))
        self.restarted = 1
        self.stepwriter.iter -= 1
        self.stepwriter.newline()

    ### line search utilities

    def initial_step(self):
        """ Determines first trial step in line search; see eg Nocedal and 
          Wright 2e section 3.5
        """
        alpha = self.loadtxt('alpha')
        s_new = self.loadtxt('s_new')
        s_old = self.loadtxt('s_old')
        s_ratio = s_new / s_old
        return 2. * s_ratio * alpha

    def step_lens(self, sort=True):
        """ Returns previous step lengths from search history
        """
        x, f = zip(*self.search_history)
        x = np.array(x)
        f = np.array(f)
        f_sorted = f[abs(x).argsort()]
        x_sorted = x[abs(x).argsort()]
        if sort:
            return x_sorted
        else:
            return x

    def func_vals(self, sort=True):
        """ Returns previous function values from search history
        """
        x, f = zip(*self.search_history)
        x = np.array(x)
        f = np.array(f)
        f_sorted = f[abs(x).argsort()]
        x_sorted = x[abs(x).argsort()]
        if sort:
            return f_sorted
        else:
            return f

    ### utilities

    def dot(self, x, y):
        """ Computes inner product between vectors
        """
        return np.dot(np.squeeze(x), np.squeeze(y))

    def load(self, filename):
        return loadnpy(PATH.OPTIMIZE + '/' + filename)

    def save(self, filename, v):
        savenpy(PATH.OPTIMIZE + '/' + filename, v)

    def loadtxt(self, filename):
        return loadtxt(PATH.OPTIMIZE + '/' + filename)

    def savetxt(self, filename, c):
        savetxt(PATH.OPTIMIZE + '/' + filename, c)