Module conduction.inversion.nd_inverse_conduction

Copyright 2017 Ben Mather

This file is part of Conduction https://git.dias.ie/itherc/conduction/

Conduction is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version.

Conduction is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with Conduction. If not, see http://www.gnu.org/licenses/.

Expand source code
"""
Copyright 2017 Ben Mather

This file is part of Conduction <https://git.dias.ie/itherc/conduction/>

Conduction is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or any later version.

Conduction is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with Conduction.  If not, see <http://www.gnu.org/licenses/>.
"""

try: range = xrange
except: pass

import numpy as np
from ..interpolation import RegularGridInterpolator, KDTreeInterpolator
from ..mesh import MeshVariable
from ..tools import sum_duplicates
from .objective_variables import InvPrior, InvObservation
from .grad_ad import gradient_ad as ad_grad

from mpi4py import MPI
from petsc4py import PETSc
comm = MPI.COMM_WORLD


class InversionND(object):

    def __init__(self, lithology, mesh, lithology_index=None, **kwargs):
        self.mesh = mesh

        # update internal mask structures
        self.update_lithology(lithology, lithology_index)


        # Custom linear / nearest-neighbour interpolator
        self.interp = RegularGridInterpolator(mesh.grid_coords[::-1],\
                                              np.zeros(mesh.n),\
                                              bounds_error=False, fill_value=np.nan)

        self.ndinterp = KDTreeInterpolator(mesh.coords, np.zeros(mesh.nn),\
                                           bounds_error=False, fill_value=0.0)

        # Get weights for ghost points
        mesh.lvec.set(1.0)
        mesh.gvec.set(0.0)
        mesh.dm.localToGlobal(mesh.lvec, mesh.gvec, addv=True)
        mesh.dm.globalToLocal(mesh.gvec, mesh.lvec)
        self.ghost_weights = np.rint(mesh.lvec.array)


        # We assume uniform grid spacing for now
        delta = []
        for i in range(0, mesh.dim):
            dx = np.diff(mesh.grid_coords[i])
            delta.append(dx.mean())
        self.grid_delta = delta


        # objective function dictionaries
        self.observation = {}
        self.prior = {}


        # Initialise linear solver
        self.ksp = self._initialise_ksp(**kwargs)

        # these should be depreciated soon
        self.temperature = self.mesh.gvec.duplicate()
        self._temperature = self.mesh.gvec.duplicate()
        self.iii = 0


    def update_lithology(self, new_lithology, lithology_index=None):
        """
        Update the configuration of lithologies.

        Internal mask structures are updated to reflect the change in
        lithology configuration.

        Arguments
        ---------
         new_lithology   : field on the mesh with integers indicating
            : the position of particular lithologies
         lithology_index : array corresponding to the total number of
            : integers in new_lithology
        
        Notes
        -----
         lithology_index is determined from the min/max of elements
         in new_lithology if lithology_index=None
        """

        new_lithology = np.array(new_lithology).ravel()

        # sync across processors
        new_lithology = self.mesh.sync(new_lithology)
        new_lithology = new_lithology.astype(np.int)

        if type(lithology_index) == type(None):
            # query global vector for minx/max
            iloc, lith_min = self.mesh.gvec.min()
            iloc, lith_max = self.mesh.gvec.max()

            # create lithology index
            lithology_index = np.arange(int(lith_min), int(lith_max)+1)


        nl = len(lithology_index)
        lithology_mask = [i for i in range(nl)]

        # create lithology mask
        for i, index in enumerate(lithology_index):
            lithology_mask[i] = np.nonzero(new_lithology == index)[0]

        self.lithology_mask = lithology_mask
        self.lithology_index = lithology_index
        self._lithology = new_lithology

        return

    @property
    def lithology(self):
        return self._lithology
    @lithology.setter
    def lithology(self, new_lithology):
        self.update_lithology(new_lithology)


    def _initialise_ksp(self, matrix=None, atol=1e-10, rtol=1e-50, **kwargs):
        """
        Initialise linear solver object
        """
        if matrix is None:
            matrix = self.mesh.mat

        solver = kwargs.pop('solver', 'gmres')
        precon = kwargs.pop('pc', None)

        ksp = PETSc.KSP().create(comm)
        ksp.setType(solver)
        ksp.setOperators(matrix)
        ksp.setTolerances(atol, rtol)
        if precon is not None:
            pc = ksp.getPC()
            pc.setType(precon)
        ksp.setFromOptions()
        return ksp

    def get_boundary_conditions(self):
        """
        Retrieve the boundary conditions so they can be restored.
        This is only useful in the adjoint linear solve where we must assert
        Dirichlet BCs (I think)

        order is [minX, maxX, minY, maxY, minZ, maxZ]

        Returns
        -------
         bc_vals : values at the boundary conditions
         bc_flux : whether it is a Neumann boundary condition

        """
        dim = self.mesh.dim
        bc_vals = np.empty(dim*2)
        bc_flux = np.empty(dim*2, dtype=bool)

        wall = [("minX", "maxX"), ("minY", "maxY"), ("minZ", "maxZ")]

        for i in range(0, dim):
            w0, w1 = wall[i]
            i0, i1 = i*2, i*2+1

            bc_vals[i0] = self.mesh.bc[w0]["val"]
            bc_flux[i0] = self.mesh.bc[w0]["flux"]

            bc_vals[i1] = self.mesh.bc[w1]["val"]
            bc_flux[i1] = self.mesh.bc[w1]["flux"]

        return bc_vals, bc_flux


    def set_boundary_conditions(self, bc_vals, bc_flux):
        """
        Set the boundary conditions easily using two vectors
        order is [minX, maxX, minY, maxY, minZ, maxZ]

        Parameters
        -------
         bc_vals : values at the boundary conditions
         bc_flux : whether it is a Neumann boundary condition

        """
        dim = self.mesh.dim
        if len(bc_vals) != len(bc_flux) or len(bc_vals) != dim*2:
            raise ValueError("Input vectors should be of size {}".format(dim*2))

        wall = [("minX", "maxX"), ("minY", "maxY"), ("minZ", "maxZ")]

        for i in range(0, dim):
            w0, w1 = wall[i]
            i0, i1 = i*2, i*2+1

            self.mesh.bc[w0]["val"]  = bc_vals[i0]
            self.mesh.bc[w0]["flux"] = bc_flux[i0]

            self.mesh.bc[w1]["val"]  = bc_vals[i1]
            self.mesh.bc[w1]["flux"] = bc_flux[i1]


    def add_observation(self, **kwargs):
        """
        Add an observation to the Inversion routine.
        These will automatically be called when the objective function is called
        and will handle interpolation.

        """
        interp = self.interp
        interp.values = self.ghost_weights.reshape(self.mesh.n)

        for arg in kwargs:
            obs = kwargs[arg]
            if type(obs) is not InvObservation:
                raise TypeError("Need to pass {} instead of {}".format(InvObservation,type(obs)))

            # add interpolation information to obs
            w = interp(obs.coords[:,::-1])
            w = 1.0/np.floor(w + 1e-12)
            offproc = np.isnan(w)
            w[offproc] = 0.0 # these are weighted with zeros
            obs.w = w # careful with 2x ghost nodes+

            # store in dictionary
            self.observation[arg] = obs


    def add_prior(self, **kwargs):
        """
        Add a prior to the Inversion routine
        """

        for arg in kwargs:
            prior = kwargs[arg]
            if type(prior) is not InvPrior:
                raise TypeError("Need to pass {} instead of {}".format(InvPrior, type(prior)))

            prior.w = 1.0

            # store in dictionary
            self.prior[arg] = prior


    def objective_routine(self, **kwargs):
        """
        This always comes at the end of the forward model (beginning of the adjoint)
        so we can safely roll interpolation, cost into one method.

        Argument is a field if it is an observation - so that we can interpolate it.
        """

        # ensure an objective function is provided
        # if self.objective_function is None:
            # raise ValueError("Pass an objective function")

        c = np.array(0.0) # local prior values same as global
        c_obs = np.array(0.0) # obs have to be summed over all procs
        c_all = np.array(0.0) # sum of all obs

        for arg in kwargs:
            val = kwargs[arg]
            if arg in self.prior:
                prior = self.prior[arg]

                if prior.cov is None:
                    c += self.objective_function(val, prior.v, prior.dv)
                else:
                    c += self.objective_function_lstsq(val, prior.v, prior.cov)
            elif arg in self.observation:
                obs = self.observation[arg]

                # interpolation
                ival = self.interpolate(val, obs.coords)

                # weighting for ghost nodes
                if obs.cov is None:
                    c_obs += self.objective_function(ival*obs.w, obs.v*obs.w, obs.dv)
                else:
                    c_obs += self.objective_function_lstsq(ival*obs.w, obs.v*obs.w, obs.cov)



        comm.Allreduce([c_obs, MPI.DOUBLE], [c_all, MPI.DOUBLE], op=MPI.SUM)
        c += c_all
        return c

    def objective_routine_ad(self, **kwargs):

        dcdv = 0.0

        for arg in kwargs:
            val = kwargs[arg]
            if arg in self.prior:
                prior = self.prior[arg]
                if prior.cov is None:
                    dcdv = self.objective_function_ad(val, prior.v, prior.dv)
                else:
                    dcdv = self.objective_function_lstsq_ad(val, prior.v, prior.cov)

            elif arg in self.observation:
                obs = self.observation[arg]

                ival = self.interpolate(val, obs.coords)


                if obs.cov is None:
                    dcdinterp = self.objective_function_ad(ival, obs.v, obs.dv)
                else:
                    dcdinterp = self.objective_function_lstsq_ad(ival*obs.w, obs.v*obs.w, obs.cov)

                # interpolation
                dcdv = self.interpolate_ad(dcdinterp, val, obs.coords)
                # print arg, np.shape(val), np.shape(ival), np.shape(dcdv)

                # sync
                dcdv = self.mesh.sync(dcdv)
            else:
                dcdv = np.zeros_like(val)

        return dcdv


    def create_covariance_matrix(self, sigma_x0, width=1, indexing='xy', fn=None, *args):
        """
        Create a covariance matrix assuming some function for variables on the mesh
        By default this is Gaussian.

        Arguments
        ---------
         sigma_x0 : uncertainty values to insert into matrix
         width    : width of stencil for matrix (int)
            i.e. extended number of neighbours for each node
         indexing : use the xy coordinates of the mesh nodes or indices
            set to 'xy' or 'ij'
         fn       : function to apply (default is Gaussian)
         *args    : input arguments to pass to fn

        Returns
        -------
            mat : covariance matrix
        """

        def gaussian_fn(sigma_x0, dist, length_scale):
            return sigma_x0**2 * np.exp(-dist**2/(2*length_scale**2))

        if type(fn) == type(None):
            fn = gaussian_fn

        nodes = self.mesh.nodes
        nn = self.mesh.nn
        n = self.mesh.n
        dim = self.mesh.dim

        if indexing == "xy":
            coords = self.mesh.coords
        elif indexing == "ij":
            ic = []
            for i in range(dim):
                ic.append( np.arange(n[i]) )
            cij = np.meshgrid(*ic, indexing="ij")

            for i in range(dim):
                cij[i] = cij[i].ravel()
            coords = np.column_stack(cij)

        # setup new stencil
        stencil_width = 2*self.mesh.dim*width + 1
        rows = np.empty((stencil_width, self.mesh.nn), dtype=PETSc.IntType)
        cols = np.empty((stencil_width, self.mesh.nn), dtype=PETSc.IntType)
        vals = np.empty((stencil_width, self.mesh.nn))
        index = np.pad(nodes.reshape(n), width, 'constant', constant_values=-1)
        sigma = np.pad(sigma_x0.reshape(n), width, 'constant', constant_values=0)

        closure = []
        for w in range(width, 0, -1):
            closure_array = self.mesh._get_closure_array(dim, w, width)
            closure.extend(closure_array[:-1])
        closure.append(closure_array[-1]) # centre node at last

        # create closure object
        closure = self.mesh._create_closure_object(closure, width)


        for i in range(0, stencil_width):
            obj = closure[i]

            rows[i] = nodes
            cols[i] = index[obj].ravel()

            distance = np.linalg.norm(coords[cols[i]] - coords, axis=1)
            vals[i] = fn(sigma[obj].ravel(), distance, *args)

        vals[cols < 0] = 0.0
        vals[-1] = 0.0

        row = rows.ravel()
        col = cols.ravel()
        val = vals.ravel()

        # mask off-grid entries and sum duplicates
        mask = col >= 0
        row, col, val = sum_duplicates(row[mask], col[mask], val[mask])

        nnz = np.bincount(row)
        indptr = np.insert(np.cumsum(nnz),0,0)

        nnz = (stencil_width, dim*2)
        mat = self.mesh._initialise_matrix(nnz=nnz)
        mat.assemblyBegin()
        mat.setValuesLocalCSR(indptr.astype(PETSc.IntType), col, val)
        mat.assemblyEnd()

        # set diagonal vector
        lvec = self.mesh.lvec
        gvec = self.mesh.gvec
        lvec.setArray(sigma_x0**2)
        self.mesh.dm.localToGlobal(lvec, gvec)
        mat.setDiagonal(gvec)
        return mat


    def create_covariance_matrix_kdtree(self, sigma_x0, width=1, indexing='xy', fn=None, *args):
        """
        Create a covariance matrix assuming some function for variables on the mesh.
        By default this is Gaussian.

        This uses a KDTree to determine distance between nodes, rather than the
        matrix stencil indexing used by create_covariance_matrix
        

        Arguments
        ---------
         sigma_x0 : uncertainty values to insert into matrix
         width    : width of stencil for matrix (int)
            i.e. extended number of neighbours for each node
         indexing : use the xy coordinates of the mesh nodes or indices
            set to 'xy' or 'ij'
         fn       : function to apply (default is Gaussian)
         *args    : input arguments to pass to fn

        Returns
        -------
            mat : covariance matrix
        """

        def gaussian_fn(sigma_x0, dist, length_scale):
            return sigma_x0**2 * np.exp(-dist**2/(2*length_scale**2))

        if type(fn) == type(None):
            fn = gaussian_fn

        nodes = self.mesh.nodes
        nn = self.mesh.nn
        n = self.mesh.n
        dim = self.mesh.dim

        if indexing == "xy":
            coords = self.mesh.coords
            tree = self.ndinterp.tree
        elif indexing == "ij":
            from scipy.spatial import cKDTree

            ic = []
            for i in range(dim):
                ic.append( np.arange(n[i]) )
            cij = np.meshgrid(*ic, indexing="ij")

            for i in range(dim):
                cij[i] = cij[i].ravel()
            coords = np.column_stack(cij)
            tree = cKDTree(coords)


        # find distance between coords and centroid
        dist = np.linalg.norm(coords - coords.mean(axis=0), axis=1)
        nnz = int(1.5*(dist <= max_dist).sum())

        mat = self.mesh._initialise_matrix(nnz=(nnz,1))
        mat.assemblyBegin()

        for i in range(0, nn):
            idx = tree.query_ball_point(coords[i], max_dist)
            dist = np.linalg.norm(coords[i] - coords[idx], axis=1)
            
            row = i
            col = idx
            val = fn(sigma[idx], dist, *args, **kwargs)
            
            mat.setValues(row, col, val)

        mat.assemblyEnd()
        return mat


    def interpolate(self, field, xi, method="nearest"):
        self.ndinterp.values = field #.reshape(self.mesh.n)
        return self.ndinterp(xi, method=method)

    def interpolate_ad(self, dxi, field, xi, method="nearest"):
        self.ndinterp.values = field #.reshape(self.mesh.n)
        return self.ndinterp.adjoint(xi, dxi, method=method) #.ravel()

    def in_bounds(self, xi):
        """
        Find if coordinates are inside the local processor bounds
        """
        idx, d, bounds = self.ndinterp._find_indices(xi)
        return bounds


    def objective_function(self, x, x0, sigma_x0):
        return np.sum(0.5*(x - x0)**2/sigma_x0**2)

    def objective_function_ad(self, x, x0, sigma_x0):
        return 0.5*(2.0*x - 2.0*x0)/sigma_x0**2


    def objective_function_lstsq(self, x, x0, cov):
        """
        Nonlinear least squares objective function
        """
        ksp = self._initialise_ksp(cov, pc='lu')
        misfit = np.array(x - x0)
        lhs, rhs = cov.createVecs()
        rhs.set(0.0)
        lindices = np.arange(0, misfit.size, dtype=PETSc.IntType)
        rhs.setValues(lindices, misfit, PETSc.InsertMode.ADD_VALUES)
        rhs.assemble()
        ksp.solve(rhs, lhs)
        sol = rhs*lhs
        sol.scale(0.5)

        ksp.destroy()
        lhs.destroy()
        rhs.destroy()

        return sol.sum()/comm.size

    def objective_function_lstsq_ad(self, x, x0, cov):
        """
        Adjoint of the nonlinear least squares objective function
        """
        ksp = self._initialise_ksp(cov, pc='lu')

        misfit = np.array(x - x0)
        lhs, rhs = cov.createVecs()
        rhs.set(0.0)
        lindices = np.arange(0, misfit.size, dtype=PETSc.IntType)
        rhs.setValues(lindices, misfit, PETSc.InsertMode.ADD_VALUES)
        rhs.assemble()
        ksp.solve(rhs, lhs)

        toall, allvec = PETSc.Scatter.toAll(lhs)
        toall.scatter(lhs, allvec, PETSc.InsertMode.INSERT)

        ksp.destroy()
        lhs.destroy()
        return allvec.array


    def map(self, *args):
        """
        Requires a tuple of vectors corresponding to an inversion variable
        these are mapped to the mesh.

        tuple(vec1, vec2, vecN) --> tuple(field1, field2, fieldN)
        """

        nf = len(args)
        nl = len(self.lithology_index)

        # preallocate local memory
        mesh_variables = np.zeros((nf, self.mesh.nn))

        # unpack vector to field
        for i in range(0, nl):
            idx = self.lithology_mask[i]
            for f in range(nf):
                mesh_variables[f,idx] = args[f][i]

        # sync fields across processors
        for f in range(nf):
            mesh_variables[f] = self.mesh.sync(mesh_variables[f])

        return list(mesh_variables)

    def map_ad(self, *args):
        """
        Map mesh variables back to the list
        """
        
        nf = len(args)
        nl = len(self.lithology_index)

        # sync fields across processors
        mesh_variables = np.zeros((nf, self.mesh.nn))
        for f in range(nf):
            mesh_variables[f] = self.mesh.sync(args[f])

        lith_variables = np.zeros((nf, self.lithology_index.size))
        all_lith_variables = np.zeros_like(lith_variables)

        for i in range(0, nl):
            idx = self.lithology_mask[i]
            for f in range(nf):
                lith_variables[f,i] += (mesh_variables[f]/self.ghost_weights)[idx].sum()

        comm.Allreduce([lith_variables, MPI.DOUBLE], [all_lith_variables, MPI.DOUBLE], op=MPI.SUM)

        return list(all_lith_variables)


    def create_wall_map(self, wall, *args):
        coords = self.mesh.coords
        dim = self.mesh.dim

        bbox = self.mesh.dm.getBoundingBox()
        sizes = self.mesh.dm.getSizes()

        # Setup boundary dictionary
        bc = dict()

        wall = [("minX", "maxX"), ("minY", "maxY"), ("minZ", "maxZ")]

        for i in range(0, dim):
            w0, w1 = wall[i]
            c0, c1 = bbox[i]
            m0, m1 = coords[:,i] == c0, coords[:,i] == c1

        
        self.boundary_index = boundary_index


    def map_wall(self, wall, *args):
        """
        Map lists of arguments to a boundary wall


        """

        if wall not in self.mesh.bc:
            raise ValueError("wall must be one of {}".format(self.mesh.bc.keys()))
        if len(args) +1 != self.mesh.dim:
            # +1 because it's a plane
            raise ValueError("dimensions of lists must equal number of dimensions")

        axis = None

        sizes = list(self.mesh.dm.getSizes())
        sizes.pop(axis)
        extent = np.reshape(self.mesh.extent, (-1,2))
        extent_bc = np.delete(extent, axis, axis=0)

        gcoords = []
        sizes = []
        for i in self.mesh.dim:
            if i != axis:
                coords = self.mesh.grid_coords[i]
                gcoords.append(coords)
                sizes.append(coords.size)

        ix = np.meshgrid(gcoords)
        
        # 0. divide wall into chunks based on the length of args
        # 1. find if proc contains bc mask
        # 2. map chunk to the bc



        bc_mask = self.mesh.bc[bc]['mask']
        if bc_mask.any():
            pass



    def linear_solve(self, matrix=None, rhs=None):

        if matrix == None:
            matrix = self.mesh.construct_matrix()
        if rhs == None:
            rhs = self.mesh.construct_rhs()

        gvec = self.mesh.gvec
        lvec = self.mesh.lvec

        res = self.mesh.temperature
        # res._gdata.setArray(rhs._gdata)

        self.ksp.setOperators(matrix)
        self.ksp.solve(rhs._gdata, res._gdata)
        return res[:].copy()

    def linear_solve_ad(self, T, dT, matrix=None, rhs=None):
        """
        If dT  = 0, adjoint=False : no need for this routine
        If dT != 0 and inside lithology, lith_size > 0
        """
        adjoint = np.array(False)
        lith_size = np.array(0.0)

        idxT = np.nonzero(dT != 0.0)[0]
        nT = idxT.any()
        comm.Allreduce([nT, MPI.BOOL], [adjoint, MPI.BOOL], op=MPI.LOR)
        if adjoint:
            if matrix == None:
                matrix = self.mesh.construct_matrix(in_place=True)
            if rhs == None:
                rhs = self.mesh.rhs
            rhs[:] = dT

            gvec = self.mesh.gvec
            lvec = self.mesh.lvec

            res = self.mesh.temperature
            res[:] = T

            # adjoint b vec
            db_ad = lvec.duplicate()

            gvec.setArray(rhs._gdata)
            self.ksp.solveTranspose(rhs._gdata, gvec)
            self.mesh.dm.globalToLocal(gvec, db_ad)

            # adjoint A mat
            dk_ad = np.zeros_like(T)

            matrix.scale(-1.0)
            # self.mesh.boundary_condition('maxZ', 0.0, flux=False) # not ND!!
            dT_ad = dT[:]
            kappa = np.zeros_like(T)
            
            nl = len(self.lithology_index)
            for i in range(0, nl):
                # find if there are nonzero dT that intersect a lithology
                idxM  = self.lithology_mask[i]
                idx_n = np.intersect1d(idxT, idxM)
                gnodes = self.ghost_weights[idx_n]
                local_size = np.array(float(idx_n.size)) - np.sum(1.0 - 1.0/gnodes) # ghost nodes
                comm.Allreduce([local_size, MPI.DOUBLE], [lith_size, MPI.DOUBLE], op=MPI.SUM)
                # print comm.rank, i, lith_size, idx_n.size

                if lith_size > 0:
                    kappa.fill(0.0)
                    kappa[idxM] = 1.0
                    self.mesh.diffusivity[:] = kappa
                    dAdkl = self.mesh.construct_matrix(in_place=False, derivative=True)
                    dAdklT = dAdkl * res._gdata
                    gvec.setArray(dAdklT) # try make the solution somewhat close
                    self.ksp.solve(dAdklT, gvec)
                    self.mesh.dm.globalToLocal(gvec, lvec)

                    # need to call sum on the global vec
                    dk_local = (dT_ad*lvec.array)/lith_size
                    lvec.setArray(dk_local)
                    self.mesh.dm.localToGlobal(lvec, gvec)
                    gdot = gvec.sum()

                    if local_size > 0:
                        # splatter inside vector
                        dk_ad[idx_n] += gdot

            dk_ad = self.mesh.sync(dk_ad)


            return dk_ad, db_ad.array
        else:
            return np.zeros_like(T), np.zeros_like(T)


    def gradient(self, f):
        """
        Calculate the derivatives of f in each dimension.

        Parameters
        ----------
         f  : ndarray shape(n,)

        Returns
        -------
         grad_f : ndarray shape(3,n)

        """
        grad = np.gradient(f.reshape(self.mesh.n), *self.grid_delta[::-1])
        return np.array(grad).reshape(self.mesh.dim, -1)

    def gradient_ad(self, df, f):
        inshape = [self.mesh.dim] + list(self.mesh.n)
        grad_ad = ad_grad(df.reshape(inshape), *self.grid_delta[::-1])
        return grad_ad.ravel()


    def heatflux(self, T, k):
        """
        Calculate heat flux.

        Arguments
        ---------
         T  : ndarray shape(n,) temperature
         k  : ndarray shape(n,) conductivity

        Returns
        -------
         q  : ndarray shape(3,n), heatflux vectors
        """
        return -k*self.gradient(T)

    def heatflux_ad(self, dq, q, T, k):

        dqddelT = -k
        dqdk = -self.gradient(T)

        ddelT = dqddelT*dq
        dk = dqdk*dq

        inshape = [self.mesh.dim] + list(self.mesh.n)

        dT = self.gradient_ad(ddelT, T)

        return dT.ravel(), dk.sum(axis=0)


    def add_perplex_table(self, TPtable):
        """
        Add Perplex table.

        Performs checks to make sure all lithologies exist inside
        the table.
        """
        from ..tools import PerplexTable
        if type(TPtable) is PerplexTable:
            for idx in self.lithology_index:
                if idx not in TPtable.table:
                    raise ValueError('{} not in TPtable'.format(idx))
            self.TPtable = TPtable
        else:
            raise ValueError('TPtable is incorrect type')

    def lookup_velocity(self, T=None, P=None):
        """
        Lookup velocity from VelocityTable object (vtable)

        Parameters
        ----------
         T  : temperature (optional)
           taken from active mesh variable if not given
         P  : pressure (optional)
           calculated from depth assuming a constant density of 2700 kg/m^3
        
        Returns
        -------
         table  : everything in the table for given nodes

        """
        if T is None:
            T = self.mesh.temperature[:]
        if P is None:
            z = np.absolute(self.mesh.coords[:,-1])

            rho = 2700.0
            r = 6.38e6 # radius of the Earth
            M = 5.98e24 # mass of the Earth
            G = 6.673e-11 # gravitational constant
            g = G*M/(r-z)**2
            P = rho*g*z*1e-5

        nl = len(self.lithology_index)
        nf = self.TPtable.ncol

        # preallocate memory
        V = np.zeros((nf, self.mesh.nn))

        for i in range(0, nl):
            idx = self.lithology_mask[i]
            lith_idx = self.lithology_index[i]
            V[:,idx] = self.TPtable(T[idx], P[idx], lith_idx).T

        return V

Classes

class InversionND (lithology, mesh, lithology_index=None, **kwargs)
Expand source code
class InversionND(object):

    def __init__(self, lithology, mesh, lithology_index=None, **kwargs):
        self.mesh = mesh

        # update internal mask structures
        self.update_lithology(lithology, lithology_index)


        # Custom linear / nearest-neighbour interpolator
        self.interp = RegularGridInterpolator(mesh.grid_coords[::-1],\
                                              np.zeros(mesh.n),\
                                              bounds_error=False, fill_value=np.nan)

        self.ndinterp = KDTreeInterpolator(mesh.coords, np.zeros(mesh.nn),\
                                           bounds_error=False, fill_value=0.0)

        # Get weights for ghost points
        mesh.lvec.set(1.0)
        mesh.gvec.set(0.0)
        mesh.dm.localToGlobal(mesh.lvec, mesh.gvec, addv=True)
        mesh.dm.globalToLocal(mesh.gvec, mesh.lvec)
        self.ghost_weights = np.rint(mesh.lvec.array)


        # We assume uniform grid spacing for now
        delta = []
        for i in range(0, mesh.dim):
            dx = np.diff(mesh.grid_coords[i])
            delta.append(dx.mean())
        self.grid_delta = delta


        # objective function dictionaries
        self.observation = {}
        self.prior = {}


        # Initialise linear solver
        self.ksp = self._initialise_ksp(**kwargs)

        # these should be depreciated soon
        self.temperature = self.mesh.gvec.duplicate()
        self._temperature = self.mesh.gvec.duplicate()
        self.iii = 0


    def update_lithology(self, new_lithology, lithology_index=None):
        """
        Update the configuration of lithologies.

        Internal mask structures are updated to reflect the change in
        lithology configuration.

        Arguments
        ---------
         new_lithology   : field on the mesh with integers indicating
            : the position of particular lithologies
         lithology_index : array corresponding to the total number of
            : integers in new_lithology
        
        Notes
        -----
         lithology_index is determined from the min/max of elements
         in new_lithology if lithology_index=None
        """

        new_lithology = np.array(new_lithology).ravel()

        # sync across processors
        new_lithology = self.mesh.sync(new_lithology)
        new_lithology = new_lithology.astype(np.int)

        if type(lithology_index) == type(None):
            # query global vector for minx/max
            iloc, lith_min = self.mesh.gvec.min()
            iloc, lith_max = self.mesh.gvec.max()

            # create lithology index
            lithology_index = np.arange(int(lith_min), int(lith_max)+1)


        nl = len(lithology_index)
        lithology_mask = [i for i in range(nl)]

        # create lithology mask
        for i, index in enumerate(lithology_index):
            lithology_mask[i] = np.nonzero(new_lithology == index)[0]

        self.lithology_mask = lithology_mask
        self.lithology_index = lithology_index
        self._lithology = new_lithology

        return

    @property
    def lithology(self):
        return self._lithology
    @lithology.setter
    def lithology(self, new_lithology):
        self.update_lithology(new_lithology)


    def _initialise_ksp(self, matrix=None, atol=1e-10, rtol=1e-50, **kwargs):
        """
        Initialise linear solver object
        """
        if matrix is None:
            matrix = self.mesh.mat

        solver = kwargs.pop('solver', 'gmres')
        precon = kwargs.pop('pc', None)

        ksp = PETSc.KSP().create(comm)
        ksp.setType(solver)
        ksp.setOperators(matrix)
        ksp.setTolerances(atol, rtol)
        if precon is not None:
            pc = ksp.getPC()
            pc.setType(precon)
        ksp.setFromOptions()
        return ksp

    def get_boundary_conditions(self):
        """
        Retrieve the boundary conditions so they can be restored.
        This is only useful in the adjoint linear solve where we must assert
        Dirichlet BCs (I think)

        order is [minX, maxX, minY, maxY, minZ, maxZ]

        Returns
        -------
         bc_vals : values at the boundary conditions
         bc_flux : whether it is a Neumann boundary condition

        """
        dim = self.mesh.dim
        bc_vals = np.empty(dim*2)
        bc_flux = np.empty(dim*2, dtype=bool)

        wall = [("minX", "maxX"), ("minY", "maxY"), ("minZ", "maxZ")]

        for i in range(0, dim):
            w0, w1 = wall[i]
            i0, i1 = i*2, i*2+1

            bc_vals[i0] = self.mesh.bc[w0]["val"]
            bc_flux[i0] = self.mesh.bc[w0]["flux"]

            bc_vals[i1] = self.mesh.bc[w1]["val"]
            bc_flux[i1] = self.mesh.bc[w1]["flux"]

        return bc_vals, bc_flux


    def set_boundary_conditions(self, bc_vals, bc_flux):
        """
        Set the boundary conditions easily using two vectors
        order is [minX, maxX, minY, maxY, minZ, maxZ]

        Parameters
        -------
         bc_vals : values at the boundary conditions
         bc_flux : whether it is a Neumann boundary condition

        """
        dim = self.mesh.dim
        if len(bc_vals) != len(bc_flux) or len(bc_vals) != dim*2:
            raise ValueError("Input vectors should be of size {}".format(dim*2))

        wall = [("minX", "maxX"), ("minY", "maxY"), ("minZ", "maxZ")]

        for i in range(0, dim):
            w0, w1 = wall[i]
            i0, i1 = i*2, i*2+1

            self.mesh.bc[w0]["val"]  = bc_vals[i0]
            self.mesh.bc[w0]["flux"] = bc_flux[i0]

            self.mesh.bc[w1]["val"]  = bc_vals[i1]
            self.mesh.bc[w1]["flux"] = bc_flux[i1]


    def add_observation(self, **kwargs):
        """
        Add an observation to the Inversion routine.
        These will automatically be called when the objective function is called
        and will handle interpolation.

        """
        interp = self.interp
        interp.values = self.ghost_weights.reshape(self.mesh.n)

        for arg in kwargs:
            obs = kwargs[arg]
            if type(obs) is not InvObservation:
                raise TypeError("Need to pass {} instead of {}".format(InvObservation,type(obs)))

            # add interpolation information to obs
            w = interp(obs.coords[:,::-1])
            w = 1.0/np.floor(w + 1e-12)
            offproc = np.isnan(w)
            w[offproc] = 0.0 # these are weighted with zeros
            obs.w = w # careful with 2x ghost nodes+

            # store in dictionary
            self.observation[arg] = obs


    def add_prior(self, **kwargs):
        """
        Add a prior to the Inversion routine
        """

        for arg in kwargs:
            prior = kwargs[arg]
            if type(prior) is not InvPrior:
                raise TypeError("Need to pass {} instead of {}".format(InvPrior, type(prior)))

            prior.w = 1.0

            # store in dictionary
            self.prior[arg] = prior


    def objective_routine(self, **kwargs):
        """
        This always comes at the end of the forward model (beginning of the adjoint)
        so we can safely roll interpolation, cost into one method.

        Argument is a field if it is an observation - so that we can interpolate it.
        """

        # ensure an objective function is provided
        # if self.objective_function is None:
            # raise ValueError("Pass an objective function")

        c = np.array(0.0) # local prior values same as global
        c_obs = np.array(0.0) # obs have to be summed over all procs
        c_all = np.array(0.0) # sum of all obs

        for arg in kwargs:
            val = kwargs[arg]
            if arg in self.prior:
                prior = self.prior[arg]

                if prior.cov is None:
                    c += self.objective_function(val, prior.v, prior.dv)
                else:
                    c += self.objective_function_lstsq(val, prior.v, prior.cov)
            elif arg in self.observation:
                obs = self.observation[arg]

                # interpolation
                ival = self.interpolate(val, obs.coords)

                # weighting for ghost nodes
                if obs.cov is None:
                    c_obs += self.objective_function(ival*obs.w, obs.v*obs.w, obs.dv)
                else:
                    c_obs += self.objective_function_lstsq(ival*obs.w, obs.v*obs.w, obs.cov)



        comm.Allreduce([c_obs, MPI.DOUBLE], [c_all, MPI.DOUBLE], op=MPI.SUM)
        c += c_all
        return c

    def objective_routine_ad(self, **kwargs):

        dcdv = 0.0

        for arg in kwargs:
            val = kwargs[arg]
            if arg in self.prior:
                prior = self.prior[arg]
                if prior.cov is None:
                    dcdv = self.objective_function_ad(val, prior.v, prior.dv)
                else:
                    dcdv = self.objective_function_lstsq_ad(val, prior.v, prior.cov)

            elif arg in self.observation:
                obs = self.observation[arg]

                ival = self.interpolate(val, obs.coords)


                if obs.cov is None:
                    dcdinterp = self.objective_function_ad(ival, obs.v, obs.dv)
                else:
                    dcdinterp = self.objective_function_lstsq_ad(ival*obs.w, obs.v*obs.w, obs.cov)

                # interpolation
                dcdv = self.interpolate_ad(dcdinterp, val, obs.coords)
                # print arg, np.shape(val), np.shape(ival), np.shape(dcdv)

                # sync
                dcdv = self.mesh.sync(dcdv)
            else:
                dcdv = np.zeros_like(val)

        return dcdv


    def create_covariance_matrix(self, sigma_x0, width=1, indexing='xy', fn=None, *args):
        """
        Create a covariance matrix assuming some function for variables on the mesh
        By default this is Gaussian.

        Arguments
        ---------
         sigma_x0 : uncertainty values to insert into matrix
         width    : width of stencil for matrix (int)
            i.e. extended number of neighbours for each node
         indexing : use the xy coordinates of the mesh nodes or indices
            set to 'xy' or 'ij'
         fn       : function to apply (default is Gaussian)
         *args    : input arguments to pass to fn

        Returns
        -------
            mat : covariance matrix
        """

        def gaussian_fn(sigma_x0, dist, length_scale):
            return sigma_x0**2 * np.exp(-dist**2/(2*length_scale**2))

        if type(fn) == type(None):
            fn = gaussian_fn

        nodes = self.mesh.nodes
        nn = self.mesh.nn
        n = self.mesh.n
        dim = self.mesh.dim

        if indexing == "xy":
            coords = self.mesh.coords
        elif indexing == "ij":
            ic = []
            for i in range(dim):
                ic.append( np.arange(n[i]) )
            cij = np.meshgrid(*ic, indexing="ij")

            for i in range(dim):
                cij[i] = cij[i].ravel()
            coords = np.column_stack(cij)

        # setup new stencil
        stencil_width = 2*self.mesh.dim*width + 1
        rows = np.empty((stencil_width, self.mesh.nn), dtype=PETSc.IntType)
        cols = np.empty((stencil_width, self.mesh.nn), dtype=PETSc.IntType)
        vals = np.empty((stencil_width, self.mesh.nn))
        index = np.pad(nodes.reshape(n), width, 'constant', constant_values=-1)
        sigma = np.pad(sigma_x0.reshape(n), width, 'constant', constant_values=0)

        closure = []
        for w in range(width, 0, -1):
            closure_array = self.mesh._get_closure_array(dim, w, width)
            closure.extend(closure_array[:-1])
        closure.append(closure_array[-1]) # centre node at last

        # create closure object
        closure = self.mesh._create_closure_object(closure, width)


        for i in range(0, stencil_width):
            obj = closure[i]

            rows[i] = nodes
            cols[i] = index[obj].ravel()

            distance = np.linalg.norm(coords[cols[i]] - coords, axis=1)
            vals[i] = fn(sigma[obj].ravel(), distance, *args)

        vals[cols < 0] = 0.0
        vals[-1] = 0.0

        row = rows.ravel()
        col = cols.ravel()
        val = vals.ravel()

        # mask off-grid entries and sum duplicates
        mask = col >= 0
        row, col, val = sum_duplicates(row[mask], col[mask], val[mask])

        nnz = np.bincount(row)
        indptr = np.insert(np.cumsum(nnz),0,0)

        nnz = (stencil_width, dim*2)
        mat = self.mesh._initialise_matrix(nnz=nnz)
        mat.assemblyBegin()
        mat.setValuesLocalCSR(indptr.astype(PETSc.IntType), col, val)
        mat.assemblyEnd()

        # set diagonal vector
        lvec = self.mesh.lvec
        gvec = self.mesh.gvec
        lvec.setArray(sigma_x0**2)
        self.mesh.dm.localToGlobal(lvec, gvec)
        mat.setDiagonal(gvec)
        return mat


    def create_covariance_matrix_kdtree(self, sigma_x0, width=1, indexing='xy', fn=None, *args):
        """
        Create a covariance matrix assuming some function for variables on the mesh.
        By default this is Gaussian.

        This uses a KDTree to determine distance between nodes, rather than the
        matrix stencil indexing used by create_covariance_matrix
        

        Arguments
        ---------
         sigma_x0 : uncertainty values to insert into matrix
         width    : width of stencil for matrix (int)
            i.e. extended number of neighbours for each node
         indexing : use the xy coordinates of the mesh nodes or indices
            set to 'xy' or 'ij'
         fn       : function to apply (default is Gaussian)
         *args    : input arguments to pass to fn

        Returns
        -------
            mat : covariance matrix
        """

        def gaussian_fn(sigma_x0, dist, length_scale):
            return sigma_x0**2 * np.exp(-dist**2/(2*length_scale**2))

        if type(fn) == type(None):
            fn = gaussian_fn

        nodes = self.mesh.nodes
        nn = self.mesh.nn
        n = self.mesh.n
        dim = self.mesh.dim

        if indexing == "xy":
            coords = self.mesh.coords
            tree = self.ndinterp.tree
        elif indexing == "ij":
            from scipy.spatial import cKDTree

            ic = []
            for i in range(dim):
                ic.append( np.arange(n[i]) )
            cij = np.meshgrid(*ic, indexing="ij")

            for i in range(dim):
                cij[i] = cij[i].ravel()
            coords = np.column_stack(cij)
            tree = cKDTree(coords)


        # find distance between coords and centroid
        dist = np.linalg.norm(coords - coords.mean(axis=0), axis=1)
        nnz = int(1.5*(dist <= max_dist).sum())

        mat = self.mesh._initialise_matrix(nnz=(nnz,1))
        mat.assemblyBegin()

        for i in range(0, nn):
            idx = tree.query_ball_point(coords[i], max_dist)
            dist = np.linalg.norm(coords[i] - coords[idx], axis=1)
            
            row = i
            col = idx
            val = fn(sigma[idx], dist, *args, **kwargs)
            
            mat.setValues(row, col, val)

        mat.assemblyEnd()
        return mat


    def interpolate(self, field, xi, method="nearest"):
        self.ndinterp.values = field #.reshape(self.mesh.n)
        return self.ndinterp(xi, method=method)

    def interpolate_ad(self, dxi, field, xi, method="nearest"):
        self.ndinterp.values = field #.reshape(self.mesh.n)
        return self.ndinterp.adjoint(xi, dxi, method=method) #.ravel()

    def in_bounds(self, xi):
        """
        Find if coordinates are inside the local processor bounds
        """
        idx, d, bounds = self.ndinterp._find_indices(xi)
        return bounds


    def objective_function(self, x, x0, sigma_x0):
        return np.sum(0.5*(x - x0)**2/sigma_x0**2)

    def objective_function_ad(self, x, x0, sigma_x0):
        return 0.5*(2.0*x - 2.0*x0)/sigma_x0**2


    def objective_function_lstsq(self, x, x0, cov):
        """
        Nonlinear least squares objective function
        """
        ksp = self._initialise_ksp(cov, pc='lu')
        misfit = np.array(x - x0)
        lhs, rhs = cov.createVecs()
        rhs.set(0.0)
        lindices = np.arange(0, misfit.size, dtype=PETSc.IntType)
        rhs.setValues(lindices, misfit, PETSc.InsertMode.ADD_VALUES)
        rhs.assemble()
        ksp.solve(rhs, lhs)
        sol = rhs*lhs
        sol.scale(0.5)

        ksp.destroy()
        lhs.destroy()
        rhs.destroy()

        return sol.sum()/comm.size

    def objective_function_lstsq_ad(self, x, x0, cov):
        """
        Adjoint of the nonlinear least squares objective function
        """
        ksp = self._initialise_ksp(cov, pc='lu')

        misfit = np.array(x - x0)
        lhs, rhs = cov.createVecs()
        rhs.set(0.0)
        lindices = np.arange(0, misfit.size, dtype=PETSc.IntType)
        rhs.setValues(lindices, misfit, PETSc.InsertMode.ADD_VALUES)
        rhs.assemble()
        ksp.solve(rhs, lhs)

        toall, allvec = PETSc.Scatter.toAll(lhs)
        toall.scatter(lhs, allvec, PETSc.InsertMode.INSERT)

        ksp.destroy()
        lhs.destroy()
        return allvec.array


    def map(self, *args):
        """
        Requires a tuple of vectors corresponding to an inversion variable
        these are mapped to the mesh.

        tuple(vec1, vec2, vecN) --> tuple(field1, field2, fieldN)
        """

        nf = len(args)
        nl = len(self.lithology_index)

        # preallocate local memory
        mesh_variables = np.zeros((nf, self.mesh.nn))

        # unpack vector to field
        for i in range(0, nl):
            idx = self.lithology_mask[i]
            for f in range(nf):
                mesh_variables[f,idx] = args[f][i]

        # sync fields across processors
        for f in range(nf):
            mesh_variables[f] = self.mesh.sync(mesh_variables[f])

        return list(mesh_variables)

    def map_ad(self, *args):
        """
        Map mesh variables back to the list
        """
        
        nf = len(args)
        nl = len(self.lithology_index)

        # sync fields across processors
        mesh_variables = np.zeros((nf, self.mesh.nn))
        for f in range(nf):
            mesh_variables[f] = self.mesh.sync(args[f])

        lith_variables = np.zeros((nf, self.lithology_index.size))
        all_lith_variables = np.zeros_like(lith_variables)

        for i in range(0, nl):
            idx = self.lithology_mask[i]
            for f in range(nf):
                lith_variables[f,i] += (mesh_variables[f]/self.ghost_weights)[idx].sum()

        comm.Allreduce([lith_variables, MPI.DOUBLE], [all_lith_variables, MPI.DOUBLE], op=MPI.SUM)

        return list(all_lith_variables)


    def create_wall_map(self, wall, *args):
        coords = self.mesh.coords
        dim = self.mesh.dim

        bbox = self.mesh.dm.getBoundingBox()
        sizes = self.mesh.dm.getSizes()

        # Setup boundary dictionary
        bc = dict()

        wall = [("minX", "maxX"), ("minY", "maxY"), ("minZ", "maxZ")]

        for i in range(0, dim):
            w0, w1 = wall[i]
            c0, c1 = bbox[i]
            m0, m1 = coords[:,i] == c0, coords[:,i] == c1

        
        self.boundary_index = boundary_index


    def map_wall(self, wall, *args):
        """
        Map lists of arguments to a boundary wall


        """

        if wall not in self.mesh.bc:
            raise ValueError("wall must be one of {}".format(self.mesh.bc.keys()))
        if len(args) +1 != self.mesh.dim:
            # +1 because it's a plane
            raise ValueError("dimensions of lists must equal number of dimensions")

        axis = None

        sizes = list(self.mesh.dm.getSizes())
        sizes.pop(axis)
        extent = np.reshape(self.mesh.extent, (-1,2))
        extent_bc = np.delete(extent, axis, axis=0)

        gcoords = []
        sizes = []
        for i in self.mesh.dim:
            if i != axis:
                coords = self.mesh.grid_coords[i]
                gcoords.append(coords)
                sizes.append(coords.size)

        ix = np.meshgrid(gcoords)
        
        # 0. divide wall into chunks based on the length of args
        # 1. find if proc contains bc mask
        # 2. map chunk to the bc



        bc_mask = self.mesh.bc[bc]['mask']
        if bc_mask.any():
            pass



    def linear_solve(self, matrix=None, rhs=None):

        if matrix == None:
            matrix = self.mesh.construct_matrix()
        if rhs == None:
            rhs = self.mesh.construct_rhs()

        gvec = self.mesh.gvec
        lvec = self.mesh.lvec

        res = self.mesh.temperature
        # res._gdata.setArray(rhs._gdata)

        self.ksp.setOperators(matrix)
        self.ksp.solve(rhs._gdata, res._gdata)
        return res[:].copy()

    def linear_solve_ad(self, T, dT, matrix=None, rhs=None):
        """
        If dT  = 0, adjoint=False : no need for this routine
        If dT != 0 and inside lithology, lith_size > 0
        """
        adjoint = np.array(False)
        lith_size = np.array(0.0)

        idxT = np.nonzero(dT != 0.0)[0]
        nT = idxT.any()
        comm.Allreduce([nT, MPI.BOOL], [adjoint, MPI.BOOL], op=MPI.LOR)
        if adjoint:
            if matrix == None:
                matrix = self.mesh.construct_matrix(in_place=True)
            if rhs == None:
                rhs = self.mesh.rhs
            rhs[:] = dT

            gvec = self.mesh.gvec
            lvec = self.mesh.lvec

            res = self.mesh.temperature
            res[:] = T

            # adjoint b vec
            db_ad = lvec.duplicate()

            gvec.setArray(rhs._gdata)
            self.ksp.solveTranspose(rhs._gdata, gvec)
            self.mesh.dm.globalToLocal(gvec, db_ad)

            # adjoint A mat
            dk_ad = np.zeros_like(T)

            matrix.scale(-1.0)
            # self.mesh.boundary_condition('maxZ', 0.0, flux=False) # not ND!!
            dT_ad = dT[:]
            kappa = np.zeros_like(T)
            
            nl = len(self.lithology_index)
            for i in range(0, nl):
                # find if there are nonzero dT that intersect a lithology
                idxM  = self.lithology_mask[i]
                idx_n = np.intersect1d(idxT, idxM)
                gnodes = self.ghost_weights[idx_n]
                local_size = np.array(float(idx_n.size)) - np.sum(1.0 - 1.0/gnodes) # ghost nodes
                comm.Allreduce([local_size, MPI.DOUBLE], [lith_size, MPI.DOUBLE], op=MPI.SUM)
                # print comm.rank, i, lith_size, idx_n.size

                if lith_size > 0:
                    kappa.fill(0.0)
                    kappa[idxM] = 1.0
                    self.mesh.diffusivity[:] = kappa
                    dAdkl = self.mesh.construct_matrix(in_place=False, derivative=True)
                    dAdklT = dAdkl * res._gdata
                    gvec.setArray(dAdklT) # try make the solution somewhat close
                    self.ksp.solve(dAdklT, gvec)
                    self.mesh.dm.globalToLocal(gvec, lvec)

                    # need to call sum on the global vec
                    dk_local = (dT_ad*lvec.array)/lith_size
                    lvec.setArray(dk_local)
                    self.mesh.dm.localToGlobal(lvec, gvec)
                    gdot = gvec.sum()

                    if local_size > 0:
                        # splatter inside vector
                        dk_ad[idx_n] += gdot

            dk_ad = self.mesh.sync(dk_ad)


            return dk_ad, db_ad.array
        else:
            return np.zeros_like(T), np.zeros_like(T)


    def gradient(self, f):
        """
        Calculate the derivatives of f in each dimension.

        Parameters
        ----------
         f  : ndarray shape(n,)

        Returns
        -------
         grad_f : ndarray shape(3,n)

        """
        grad = np.gradient(f.reshape(self.mesh.n), *self.grid_delta[::-1])
        return np.array(grad).reshape(self.mesh.dim, -1)

    def gradient_ad(self, df, f):
        inshape = [self.mesh.dim] + list(self.mesh.n)
        grad_ad = ad_grad(df.reshape(inshape), *self.grid_delta[::-1])
        return grad_ad.ravel()


    def heatflux(self, T, k):
        """
        Calculate heat flux.

        Arguments
        ---------
         T  : ndarray shape(n,) temperature
         k  : ndarray shape(n,) conductivity

        Returns
        -------
         q  : ndarray shape(3,n), heatflux vectors
        """
        return -k*self.gradient(T)

    def heatflux_ad(self, dq, q, T, k):

        dqddelT = -k
        dqdk = -self.gradient(T)

        ddelT = dqddelT*dq
        dk = dqdk*dq

        inshape = [self.mesh.dim] + list(self.mesh.n)

        dT = self.gradient_ad(ddelT, T)

        return dT.ravel(), dk.sum(axis=0)


    def add_perplex_table(self, TPtable):
        """
        Add Perplex table.

        Performs checks to make sure all lithologies exist inside
        the table.
        """
        from ..tools import PerplexTable
        if type(TPtable) is PerplexTable:
            for idx in self.lithology_index:
                if idx not in TPtable.table:
                    raise ValueError('{} not in TPtable'.format(idx))
            self.TPtable = TPtable
        else:
            raise ValueError('TPtable is incorrect type')

    def lookup_velocity(self, T=None, P=None):
        """
        Lookup velocity from VelocityTable object (vtable)

        Parameters
        ----------
         T  : temperature (optional)
           taken from active mesh variable if not given
         P  : pressure (optional)
           calculated from depth assuming a constant density of 2700 kg/m^3
        
        Returns
        -------
         table  : everything in the table for given nodes

        """
        if T is None:
            T = self.mesh.temperature[:]
        if P is None:
            z = np.absolute(self.mesh.coords[:,-1])

            rho = 2700.0
            r = 6.38e6 # radius of the Earth
            M = 5.98e24 # mass of the Earth
            G = 6.673e-11 # gravitational constant
            g = G*M/(r-z)**2
            P = rho*g*z*1e-5

        nl = len(self.lithology_index)
        nf = self.TPtable.ncol

        # preallocate memory
        V = np.zeros((nf, self.mesh.nn))

        for i in range(0, nl):
            idx = self.lithology_mask[i]
            lith_idx = self.lithology_index[i]
            V[:,idx] = self.TPtable(T[idx], P[idx], lith_idx).T

        return V

Instance variables

var lithology
Expand source code
@property
def lithology(self):
    return self._lithology

Methods

def add_observation(self, **kwargs)

Add an observation to the Inversion routine. These will automatically be called when the objective function is called and will handle interpolation.

Expand source code
def add_observation(self, **kwargs):
    """
    Add an observation to the Inversion routine.
    These will automatically be called when the objective function is called
    and will handle interpolation.

    """
    interp = self.interp
    interp.values = self.ghost_weights.reshape(self.mesh.n)

    for arg in kwargs:
        obs = kwargs[arg]
        if type(obs) is not InvObservation:
            raise TypeError("Need to pass {} instead of {}".format(InvObservation,type(obs)))

        # add interpolation information to obs
        w = interp(obs.coords[:,::-1])
        w = 1.0/np.floor(w + 1e-12)
        offproc = np.isnan(w)
        w[offproc] = 0.0 # these are weighted with zeros
        obs.w = w # careful with 2x ghost nodes+

        # store in dictionary
        self.observation[arg] = obs
def add_perplex_table(self, TPtable)

Add Perplex table.

Performs checks to make sure all lithologies exist inside the table.

Expand source code
def add_perplex_table(self, TPtable):
    """
    Add Perplex table.

    Performs checks to make sure all lithologies exist inside
    the table.
    """
    from ..tools import PerplexTable
    if type(TPtable) is PerplexTable:
        for idx in self.lithology_index:
            if idx not in TPtable.table:
                raise ValueError('{} not in TPtable'.format(idx))
        self.TPtable = TPtable
    else:
        raise ValueError('TPtable is incorrect type')
def add_prior(self, **kwargs)

Add a prior to the Inversion routine

Expand source code
def add_prior(self, **kwargs):
    """
    Add a prior to the Inversion routine
    """

    for arg in kwargs:
        prior = kwargs[arg]
        if type(prior) is not InvPrior:
            raise TypeError("Need to pass {} instead of {}".format(InvPrior, type(prior)))

        prior.w = 1.0

        # store in dictionary
        self.prior[arg] = prior
def create_covariance_matrix(self, sigma_x0, width=1, indexing='xy', fn=None, *args)

Create a covariance matrix assuming some function for variables on the mesh By default this is Gaussian.

Arguments

sigma_x0 : uncertainty values to insert into matrix width : width of stencil for matrix (int) i.e. extended number of neighbours for each node indexing : use the xy coordinates of the mesh nodes or indices set to 'xy' or 'ij' fn : function to apply (default is Gaussian) *args : input arguments to pass to fn

Returns

mat : covariance matrix
Expand source code
def create_covariance_matrix(self, sigma_x0, width=1, indexing='xy', fn=None, *args):
    """
    Create a covariance matrix assuming some function for variables on the mesh
    By default this is Gaussian.

    Arguments
    ---------
     sigma_x0 : uncertainty values to insert into matrix
     width    : width of stencil for matrix (int)
        i.e. extended number of neighbours for each node
     indexing : use the xy coordinates of the mesh nodes or indices
        set to 'xy' or 'ij'
     fn       : function to apply (default is Gaussian)
     *args    : input arguments to pass to fn

    Returns
    -------
        mat : covariance matrix
    """

    def gaussian_fn(sigma_x0, dist, length_scale):
        return sigma_x0**2 * np.exp(-dist**2/(2*length_scale**2))

    if type(fn) == type(None):
        fn = gaussian_fn

    nodes = self.mesh.nodes
    nn = self.mesh.nn
    n = self.mesh.n
    dim = self.mesh.dim

    if indexing == "xy":
        coords = self.mesh.coords
    elif indexing == "ij":
        ic = []
        for i in range(dim):
            ic.append( np.arange(n[i]) )
        cij = np.meshgrid(*ic, indexing="ij")

        for i in range(dim):
            cij[i] = cij[i].ravel()
        coords = np.column_stack(cij)

    # setup new stencil
    stencil_width = 2*self.mesh.dim*width + 1
    rows = np.empty((stencil_width, self.mesh.nn), dtype=PETSc.IntType)
    cols = np.empty((stencil_width, self.mesh.nn), dtype=PETSc.IntType)
    vals = np.empty((stencil_width, self.mesh.nn))
    index = np.pad(nodes.reshape(n), width, 'constant', constant_values=-1)
    sigma = np.pad(sigma_x0.reshape(n), width, 'constant', constant_values=0)

    closure = []
    for w in range(width, 0, -1):
        closure_array = self.mesh._get_closure_array(dim, w, width)
        closure.extend(closure_array[:-1])
    closure.append(closure_array[-1]) # centre node at last

    # create closure object
    closure = self.mesh._create_closure_object(closure, width)


    for i in range(0, stencil_width):
        obj = closure[i]

        rows[i] = nodes
        cols[i] = index[obj].ravel()

        distance = np.linalg.norm(coords[cols[i]] - coords, axis=1)
        vals[i] = fn(sigma[obj].ravel(), distance, *args)

    vals[cols < 0] = 0.0
    vals[-1] = 0.0

    row = rows.ravel()
    col = cols.ravel()
    val = vals.ravel()

    # mask off-grid entries and sum duplicates
    mask = col >= 0
    row, col, val = sum_duplicates(row[mask], col[mask], val[mask])

    nnz = np.bincount(row)
    indptr = np.insert(np.cumsum(nnz),0,0)

    nnz = (stencil_width, dim*2)
    mat = self.mesh._initialise_matrix(nnz=nnz)
    mat.assemblyBegin()
    mat.setValuesLocalCSR(indptr.astype(PETSc.IntType), col, val)
    mat.assemblyEnd()

    # set diagonal vector
    lvec = self.mesh.lvec
    gvec = self.mesh.gvec
    lvec.setArray(sigma_x0**2)
    self.mesh.dm.localToGlobal(lvec, gvec)
    mat.setDiagonal(gvec)
    return mat
def create_covariance_matrix_kdtree(self, sigma_x0, width=1, indexing='xy', fn=None, *args)

Create a covariance matrix assuming some function for variables on the mesh. By default this is Gaussian.

This uses a KDTree to determine distance between nodes, rather than the matrix stencil indexing used by create_covariance_matrix

Arguments

sigma_x0 : uncertainty values to insert into matrix width : width of stencil for matrix (int) i.e. extended number of neighbours for each node indexing : use the xy coordinates of the mesh nodes or indices set to 'xy' or 'ij' fn : function to apply (default is Gaussian) *args : input arguments to pass to fn

Returns

mat : covariance matrix
Expand source code
def create_covariance_matrix_kdtree(self, sigma_x0, width=1, indexing='xy', fn=None, *args):
    """
    Create a covariance matrix assuming some function for variables on the mesh.
    By default this is Gaussian.

    This uses a KDTree to determine distance between nodes, rather than the
    matrix stencil indexing used by create_covariance_matrix
    

    Arguments
    ---------
     sigma_x0 : uncertainty values to insert into matrix
     width    : width of stencil for matrix (int)
        i.e. extended number of neighbours for each node
     indexing : use the xy coordinates of the mesh nodes or indices
        set to 'xy' or 'ij'
     fn       : function to apply (default is Gaussian)
     *args    : input arguments to pass to fn

    Returns
    -------
        mat : covariance matrix
    """

    def gaussian_fn(sigma_x0, dist, length_scale):
        return sigma_x0**2 * np.exp(-dist**2/(2*length_scale**2))

    if type(fn) == type(None):
        fn = gaussian_fn

    nodes = self.mesh.nodes
    nn = self.mesh.nn
    n = self.mesh.n
    dim = self.mesh.dim

    if indexing == "xy":
        coords = self.mesh.coords
        tree = self.ndinterp.tree
    elif indexing == "ij":
        from scipy.spatial import cKDTree

        ic = []
        for i in range(dim):
            ic.append( np.arange(n[i]) )
        cij = np.meshgrid(*ic, indexing="ij")

        for i in range(dim):
            cij[i] = cij[i].ravel()
        coords = np.column_stack(cij)
        tree = cKDTree(coords)


    # find distance between coords and centroid
    dist = np.linalg.norm(coords - coords.mean(axis=0), axis=1)
    nnz = int(1.5*(dist <= max_dist).sum())

    mat = self.mesh._initialise_matrix(nnz=(nnz,1))
    mat.assemblyBegin()

    for i in range(0, nn):
        idx = tree.query_ball_point(coords[i], max_dist)
        dist = np.linalg.norm(coords[i] - coords[idx], axis=1)
        
        row = i
        col = idx
        val = fn(sigma[idx], dist, *args, **kwargs)
        
        mat.setValues(row, col, val)

    mat.assemblyEnd()
    return mat
def create_wall_map(self, wall, *args)
Expand source code
def create_wall_map(self, wall, *args):
    coords = self.mesh.coords
    dim = self.mesh.dim

    bbox = self.mesh.dm.getBoundingBox()
    sizes = self.mesh.dm.getSizes()

    # Setup boundary dictionary
    bc = dict()

    wall = [("minX", "maxX"), ("minY", "maxY"), ("minZ", "maxZ")]

    for i in range(0, dim):
        w0, w1 = wall[i]
        c0, c1 = bbox[i]
        m0, m1 = coords[:,i] == c0, coords[:,i] == c1

    
    self.boundary_index = boundary_index
def get_boundary_conditions(self)

Retrieve the boundary conditions so they can be restored. This is only useful in the adjoint linear solve where we must assert Dirichlet BCs (I think)

order is [minX, maxX, minY, maxY, minZ, maxZ]

Returns

bc_vals : values at the boundary conditions bc_flux : whether it is a Neumann boundary condition

Expand source code
def get_boundary_conditions(self):
    """
    Retrieve the boundary conditions so they can be restored.
    This is only useful in the adjoint linear solve where we must assert
    Dirichlet BCs (I think)

    order is [minX, maxX, minY, maxY, minZ, maxZ]

    Returns
    -------
     bc_vals : values at the boundary conditions
     bc_flux : whether it is a Neumann boundary condition

    """
    dim = self.mesh.dim
    bc_vals = np.empty(dim*2)
    bc_flux = np.empty(dim*2, dtype=bool)

    wall = [("minX", "maxX"), ("minY", "maxY"), ("minZ", "maxZ")]

    for i in range(0, dim):
        w0, w1 = wall[i]
        i0, i1 = i*2, i*2+1

        bc_vals[i0] = self.mesh.bc[w0]["val"]
        bc_flux[i0] = self.mesh.bc[w0]["flux"]

        bc_vals[i1] = self.mesh.bc[w1]["val"]
        bc_flux[i1] = self.mesh.bc[w1]["flux"]

    return bc_vals, bc_flux
def gradient(self, f)

Calculate the derivatives of f in each dimension.

Parameters

f : ndarray shape(n,)

Returns

grad_f : ndarray shape(3,n)

Expand source code
def gradient(self, f):
    """
    Calculate the derivatives of f in each dimension.

    Parameters
    ----------
     f  : ndarray shape(n,)

    Returns
    -------
     grad_f : ndarray shape(3,n)

    """
    grad = np.gradient(f.reshape(self.mesh.n), *self.grid_delta[::-1])
    return np.array(grad).reshape(self.mesh.dim, -1)
def gradient_ad(self, df, f)
Expand source code
def gradient_ad(self, df, f):
    inshape = [self.mesh.dim] + list(self.mesh.n)
    grad_ad = ad_grad(df.reshape(inshape), *self.grid_delta[::-1])
    return grad_ad.ravel()
def heatflux(self, T, k)

Calculate heat flux.

Arguments

T : ndarray shape(n,) temperature k : ndarray shape(n,) conductivity

Returns

q : ndarray shape(3,n), heatflux vectors

Expand source code
def heatflux(self, T, k):
    """
    Calculate heat flux.

    Arguments
    ---------
     T  : ndarray shape(n,) temperature
     k  : ndarray shape(n,) conductivity

    Returns
    -------
     q  : ndarray shape(3,n), heatflux vectors
    """
    return -k*self.gradient(T)
def heatflux_ad(self, dq, q, T, k)
Expand source code
def heatflux_ad(self, dq, q, T, k):

    dqddelT = -k
    dqdk = -self.gradient(T)

    ddelT = dqddelT*dq
    dk = dqdk*dq

    inshape = [self.mesh.dim] + list(self.mesh.n)

    dT = self.gradient_ad(ddelT, T)

    return dT.ravel(), dk.sum(axis=0)
def in_bounds(self, xi)

Find if coordinates are inside the local processor bounds

Expand source code
def in_bounds(self, xi):
    """
    Find if coordinates are inside the local processor bounds
    """
    idx, d, bounds = self.ndinterp._find_indices(xi)
    return bounds
def interpolate(self, field, xi, method='nearest')
Expand source code
def interpolate(self, field, xi, method="nearest"):
    self.ndinterp.values = field #.reshape(self.mesh.n)
    return self.ndinterp(xi, method=method)
def interpolate_ad(self, dxi, field, xi, method='nearest')
Expand source code
def interpolate_ad(self, dxi, field, xi, method="nearest"):
    self.ndinterp.values = field #.reshape(self.mesh.n)
    return self.ndinterp.adjoint(xi, dxi, method=method) #.ravel()
def linear_solve(self, matrix=None, rhs=None)
Expand source code
def linear_solve(self, matrix=None, rhs=None):

    if matrix == None:
        matrix = self.mesh.construct_matrix()
    if rhs == None:
        rhs = self.mesh.construct_rhs()

    gvec = self.mesh.gvec
    lvec = self.mesh.lvec

    res = self.mesh.temperature
    # res._gdata.setArray(rhs._gdata)

    self.ksp.setOperators(matrix)
    self.ksp.solve(rhs._gdata, res._gdata)
    return res[:].copy()
def linear_solve_ad(self, T, dT, matrix=None, rhs=None)

If dT = 0, adjoint=False : no need for this routine If dT != 0 and inside lithology, lith_size > 0

Expand source code
def linear_solve_ad(self, T, dT, matrix=None, rhs=None):
    """
    If dT  = 0, adjoint=False : no need for this routine
    If dT != 0 and inside lithology, lith_size > 0
    """
    adjoint = np.array(False)
    lith_size = np.array(0.0)

    idxT = np.nonzero(dT != 0.0)[0]
    nT = idxT.any()
    comm.Allreduce([nT, MPI.BOOL], [adjoint, MPI.BOOL], op=MPI.LOR)
    if adjoint:
        if matrix == None:
            matrix = self.mesh.construct_matrix(in_place=True)
        if rhs == None:
            rhs = self.mesh.rhs
        rhs[:] = dT

        gvec = self.mesh.gvec
        lvec = self.mesh.lvec

        res = self.mesh.temperature
        res[:] = T

        # adjoint b vec
        db_ad = lvec.duplicate()

        gvec.setArray(rhs._gdata)
        self.ksp.solveTranspose(rhs._gdata, gvec)
        self.mesh.dm.globalToLocal(gvec, db_ad)

        # adjoint A mat
        dk_ad = np.zeros_like(T)

        matrix.scale(-1.0)
        # self.mesh.boundary_condition('maxZ', 0.0, flux=False) # not ND!!
        dT_ad = dT[:]
        kappa = np.zeros_like(T)
        
        nl = len(self.lithology_index)
        for i in range(0, nl):
            # find if there are nonzero dT that intersect a lithology
            idxM  = self.lithology_mask[i]
            idx_n = np.intersect1d(idxT, idxM)
            gnodes = self.ghost_weights[idx_n]
            local_size = np.array(float(idx_n.size)) - np.sum(1.0 - 1.0/gnodes) # ghost nodes
            comm.Allreduce([local_size, MPI.DOUBLE], [lith_size, MPI.DOUBLE], op=MPI.SUM)
            # print comm.rank, i, lith_size, idx_n.size

            if lith_size > 0:
                kappa.fill(0.0)
                kappa[idxM] = 1.0
                self.mesh.diffusivity[:] = kappa
                dAdkl = self.mesh.construct_matrix(in_place=False, derivative=True)
                dAdklT = dAdkl * res._gdata
                gvec.setArray(dAdklT) # try make the solution somewhat close
                self.ksp.solve(dAdklT, gvec)
                self.mesh.dm.globalToLocal(gvec, lvec)

                # need to call sum on the global vec
                dk_local = (dT_ad*lvec.array)/lith_size
                lvec.setArray(dk_local)
                self.mesh.dm.localToGlobal(lvec, gvec)
                gdot = gvec.sum()

                if local_size > 0:
                    # splatter inside vector
                    dk_ad[idx_n] += gdot

        dk_ad = self.mesh.sync(dk_ad)


        return dk_ad, db_ad.array
    else:
        return np.zeros_like(T), np.zeros_like(T)
def lookup_velocity(self, T=None, P=None)

Lookup velocity from VelocityTable object (vtable)

Parameters

T : temperature (optional) taken from active mesh variable if not given P : pressure (optional) calculated from depth assuming a constant density of 2700 kg/m^3

Returns

table : everything in the table for given nodes

Expand source code
def lookup_velocity(self, T=None, P=None):
    """
    Lookup velocity from VelocityTable object (vtable)

    Parameters
    ----------
     T  : temperature (optional)
       taken from active mesh variable if not given
     P  : pressure (optional)
       calculated from depth assuming a constant density of 2700 kg/m^3
    
    Returns
    -------
     table  : everything in the table for given nodes

    """
    if T is None:
        T = self.mesh.temperature[:]
    if P is None:
        z = np.absolute(self.mesh.coords[:,-1])

        rho = 2700.0
        r = 6.38e6 # radius of the Earth
        M = 5.98e24 # mass of the Earth
        G = 6.673e-11 # gravitational constant
        g = G*M/(r-z)**2
        P = rho*g*z*1e-5

    nl = len(self.lithology_index)
    nf = self.TPtable.ncol

    # preallocate memory
    V = np.zeros((nf, self.mesh.nn))

    for i in range(0, nl):
        idx = self.lithology_mask[i]
        lith_idx = self.lithology_index[i]
        V[:,idx] = self.TPtable(T[idx], P[idx], lith_idx).T

    return V
def map(self, *args)

Requires a tuple of vectors corresponding to an inversion variable these are mapped to the mesh.

tuple(vec1, vec2, vecN) –> tuple(field1, field2, fieldN)

Expand source code
def map(self, *args):
    """
    Requires a tuple of vectors corresponding to an inversion variable
    these are mapped to the mesh.

    tuple(vec1, vec2, vecN) --> tuple(field1, field2, fieldN)
    """

    nf = len(args)
    nl = len(self.lithology_index)

    # preallocate local memory
    mesh_variables = np.zeros((nf, self.mesh.nn))

    # unpack vector to field
    for i in range(0, nl):
        idx = self.lithology_mask[i]
        for f in range(nf):
            mesh_variables[f,idx] = args[f][i]

    # sync fields across processors
    for f in range(nf):
        mesh_variables[f] = self.mesh.sync(mesh_variables[f])

    return list(mesh_variables)
def map_ad(self, *args)

Map mesh variables back to the list

Expand source code
def map_ad(self, *args):
    """
    Map mesh variables back to the list
    """
    
    nf = len(args)
    nl = len(self.lithology_index)

    # sync fields across processors
    mesh_variables = np.zeros((nf, self.mesh.nn))
    for f in range(nf):
        mesh_variables[f] = self.mesh.sync(args[f])

    lith_variables = np.zeros((nf, self.lithology_index.size))
    all_lith_variables = np.zeros_like(lith_variables)

    for i in range(0, nl):
        idx = self.lithology_mask[i]
        for f in range(nf):
            lith_variables[f,i] += (mesh_variables[f]/self.ghost_weights)[idx].sum()

    comm.Allreduce([lith_variables, MPI.DOUBLE], [all_lith_variables, MPI.DOUBLE], op=MPI.SUM)

    return list(all_lith_variables)
def map_wall(self, wall, *args)

Map lists of arguments to a boundary wall

Expand source code
def map_wall(self, wall, *args):
    """
    Map lists of arguments to a boundary wall


    """

    if wall not in self.mesh.bc:
        raise ValueError("wall must be one of {}".format(self.mesh.bc.keys()))
    if len(args) +1 != self.mesh.dim:
        # +1 because it's a plane
        raise ValueError("dimensions of lists must equal number of dimensions")

    axis = None

    sizes = list(self.mesh.dm.getSizes())
    sizes.pop(axis)
    extent = np.reshape(self.mesh.extent, (-1,2))
    extent_bc = np.delete(extent, axis, axis=0)

    gcoords = []
    sizes = []
    for i in self.mesh.dim:
        if i != axis:
            coords = self.mesh.grid_coords[i]
            gcoords.append(coords)
            sizes.append(coords.size)

    ix = np.meshgrid(gcoords)
    
    # 0. divide wall into chunks based on the length of args
    # 1. find if proc contains bc mask
    # 2. map chunk to the bc



    bc_mask = self.mesh.bc[bc]['mask']
    if bc_mask.any():
        pass
def objective_function(self, x, x0, sigma_x0)
Expand source code
def objective_function(self, x, x0, sigma_x0):
    return np.sum(0.5*(x - x0)**2/sigma_x0**2)
def objective_function_ad(self, x, x0, sigma_x0)
Expand source code
def objective_function_ad(self, x, x0, sigma_x0):
    return 0.5*(2.0*x - 2.0*x0)/sigma_x0**2
def objective_function_lstsq(self, x, x0, cov)

Nonlinear least squares objective function

Expand source code
def objective_function_lstsq(self, x, x0, cov):
    """
    Nonlinear least squares objective function
    """
    ksp = self._initialise_ksp(cov, pc='lu')
    misfit = np.array(x - x0)
    lhs, rhs = cov.createVecs()
    rhs.set(0.0)
    lindices = np.arange(0, misfit.size, dtype=PETSc.IntType)
    rhs.setValues(lindices, misfit, PETSc.InsertMode.ADD_VALUES)
    rhs.assemble()
    ksp.solve(rhs, lhs)
    sol = rhs*lhs
    sol.scale(0.5)

    ksp.destroy()
    lhs.destroy()
    rhs.destroy()

    return sol.sum()/comm.size
def objective_function_lstsq_ad(self, x, x0, cov)

Adjoint of the nonlinear least squares objective function

Expand source code
def objective_function_lstsq_ad(self, x, x0, cov):
    """
    Adjoint of the nonlinear least squares objective function
    """
    ksp = self._initialise_ksp(cov, pc='lu')

    misfit = np.array(x - x0)
    lhs, rhs = cov.createVecs()
    rhs.set(0.0)
    lindices = np.arange(0, misfit.size, dtype=PETSc.IntType)
    rhs.setValues(lindices, misfit, PETSc.InsertMode.ADD_VALUES)
    rhs.assemble()
    ksp.solve(rhs, lhs)

    toall, allvec = PETSc.Scatter.toAll(lhs)
    toall.scatter(lhs, allvec, PETSc.InsertMode.INSERT)

    ksp.destroy()
    lhs.destroy()
    return allvec.array
def objective_routine(self, **kwargs)

This always comes at the end of the forward model (beginning of the adjoint) so we can safely roll interpolation, cost into one method.

Argument is a field if it is an observation - so that we can interpolate it.

Expand source code
def objective_routine(self, **kwargs):
    """
    This always comes at the end of the forward model (beginning of the adjoint)
    so we can safely roll interpolation, cost into one method.

    Argument is a field if it is an observation - so that we can interpolate it.
    """

    # ensure an objective function is provided
    # if self.objective_function is None:
        # raise ValueError("Pass an objective function")

    c = np.array(0.0) # local prior values same as global
    c_obs = np.array(0.0) # obs have to be summed over all procs
    c_all = np.array(0.0) # sum of all obs

    for arg in kwargs:
        val = kwargs[arg]
        if arg in self.prior:
            prior = self.prior[arg]

            if prior.cov is None:
                c += self.objective_function(val, prior.v, prior.dv)
            else:
                c += self.objective_function_lstsq(val, prior.v, prior.cov)
        elif arg in self.observation:
            obs = self.observation[arg]

            # interpolation
            ival = self.interpolate(val, obs.coords)

            # weighting for ghost nodes
            if obs.cov is None:
                c_obs += self.objective_function(ival*obs.w, obs.v*obs.w, obs.dv)
            else:
                c_obs += self.objective_function_lstsq(ival*obs.w, obs.v*obs.w, obs.cov)



    comm.Allreduce([c_obs, MPI.DOUBLE], [c_all, MPI.DOUBLE], op=MPI.SUM)
    c += c_all
    return c
def objective_routine_ad(self, **kwargs)
Expand source code
def objective_routine_ad(self, **kwargs):

    dcdv = 0.0

    for arg in kwargs:
        val = kwargs[arg]
        if arg in self.prior:
            prior = self.prior[arg]
            if prior.cov is None:
                dcdv = self.objective_function_ad(val, prior.v, prior.dv)
            else:
                dcdv = self.objective_function_lstsq_ad(val, prior.v, prior.cov)

        elif arg in self.observation:
            obs = self.observation[arg]

            ival = self.interpolate(val, obs.coords)


            if obs.cov is None:
                dcdinterp = self.objective_function_ad(ival, obs.v, obs.dv)
            else:
                dcdinterp = self.objective_function_lstsq_ad(ival*obs.w, obs.v*obs.w, obs.cov)

            # interpolation
            dcdv = self.interpolate_ad(dcdinterp, val, obs.coords)
            # print arg, np.shape(val), np.shape(ival), np.shape(dcdv)

            # sync
            dcdv = self.mesh.sync(dcdv)
        else:
            dcdv = np.zeros_like(val)

    return dcdv
def set_boundary_conditions(self, bc_vals, bc_flux)

Set the boundary conditions easily using two vectors order is [minX, maxX, minY, maxY, minZ, maxZ]

Parameters

bc_vals : values at the boundary conditions bc_flux : whether it is a Neumann boundary condition

Expand source code
def set_boundary_conditions(self, bc_vals, bc_flux):
    """
    Set the boundary conditions easily using two vectors
    order is [minX, maxX, minY, maxY, minZ, maxZ]

    Parameters
    -------
     bc_vals : values at the boundary conditions
     bc_flux : whether it is a Neumann boundary condition

    """
    dim = self.mesh.dim
    if len(bc_vals) != len(bc_flux) or len(bc_vals) != dim*2:
        raise ValueError("Input vectors should be of size {}".format(dim*2))

    wall = [("minX", "maxX"), ("minY", "maxY"), ("minZ", "maxZ")]

    for i in range(0, dim):
        w0, w1 = wall[i]
        i0, i1 = i*2, i*2+1

        self.mesh.bc[w0]["val"]  = bc_vals[i0]
        self.mesh.bc[w0]["flux"] = bc_flux[i0]

        self.mesh.bc[w1]["val"]  = bc_vals[i1]
        self.mesh.bc[w1]["flux"] = bc_flux[i1]
def update_lithology(self, new_lithology, lithology_index=None)

Update the configuration of lithologies.

Internal mask structures are updated to reflect the change in lithology configuration.

Arguments

new_lithology : field on the mesh with integers indicating : the position of particular lithologies lithology_index : array corresponding to the total number of : integers in new_lithology

Notes

lithology_index is determined from the min/max of elements in new_lithology if lithology_index=None

Expand source code
def update_lithology(self, new_lithology, lithology_index=None):
    """
    Update the configuration of lithologies.

    Internal mask structures are updated to reflect the change in
    lithology configuration.

    Arguments
    ---------
     new_lithology   : field on the mesh with integers indicating
        : the position of particular lithologies
     lithology_index : array corresponding to the total number of
        : integers in new_lithology
    
    Notes
    -----
     lithology_index is determined from the min/max of elements
     in new_lithology if lithology_index=None
    """

    new_lithology = np.array(new_lithology).ravel()

    # sync across processors
    new_lithology = self.mesh.sync(new_lithology)
    new_lithology = new_lithology.astype(np.int)

    if type(lithology_index) == type(None):
        # query global vector for minx/max
        iloc, lith_min = self.mesh.gvec.min()
        iloc, lith_max = self.mesh.gvec.max()

        # create lithology index
        lithology_index = np.arange(int(lith_min), int(lith_max)+1)


    nl = len(lithology_index)
    lithology_mask = [i for i in range(nl)]

    # create lithology mask
    for i, index in enumerate(lithology_index):
        lithology_mask[i] = np.nonzero(new_lithology == index)[0]

    self.lithology_mask = lithology_mask
    self.lithology_index = lithology_index
    self._lithology = new_lithology

    return