Example #1
0
    def gibbs_covariance(x1, x2):
        '''
        covariance function for the Gibbs Gaussian process.
        '''
        dim = x1.shape[1]
        lsx1 = lengthscale(x1)
        lsx2 = lengthscale(x2)

        # sanitize the output for `lengthscale`
        lsx1 = np.asarray(lsx1, dtype=float)
        lsx2 = np.asarray(lsx2, dtype=float)
        assert_shape(lsx1, x1.shape, 'lengthscale(x1)')
        assert_shape(lsx2, x2.shape, 'lengthscale(x2)')

        coeff = np.ones((x1.shape[0], x2.shape[0]))
        exponent = np.zeros((x1.shape[0], x2.shape[0]))

        for i in range(dim):
            a = 2 * lsx1[:, None, i] * lsx2[None, :, i]
            b = lsx1[:, None, i]**2 + lsx2[None, :, i]**2
            coeff *= np.sqrt(a / b)

        for i in range(dim):
            a = (x1[:, None, i] - x2[None, :, i])**2
            b = lsx1[:, None, i]**2 + lsx2[None, :, i]**2
            exponent -= (a / b)

        out = sigma**2 * coeff * np.exp(exponent)
        return out
Example #2
0
def add_rows(A, B, idx):
    '''
  This function effectively returns `A` after the operation `A[idx, :]
  += B`, where `A` and `B` are both sparse matrices. This function
  exists because the current implementation of `A[idx, :] += B` expands
  out `B` and takes up way too much memory.

  Parameters
  ----------
  A: (n1, m) scipy sparse matrix

  B: (n2, m) scipy sparse matrix

  idx: (n2,) int array
    rows of `A` that `B` will be added to
    
  Returns
  -------
  (n1, m) csc sparse matrix
  
  '''
    # coerce `A` to csc to enforce a consistent output type
    A = sp.csc_matrix(A)
    # convert `B` to a coo matrix, and expand out its rows
    B = sp.coo_matrix(B)
    assert_shape(B, (None, A.shape[1]), 'B')

    idx = np.asarray(idx, dtype=int)
    assert_shape(idx, (B.shape[0], ), 'idx')

    B = sp.csc_matrix((B.data, (idx[B.row], B.col)), shape=A.shape)
    # Now add the expanded `B` to `A`,
    out = A + B
    return out
Example #3
0
def sample(mu, cov, use_cholesky=False, count=None):
    '''
    Draws a random sample from the multivariate normal distribution.

    Parameters
    ----------
    mu : (N,) array
        Mean vector.

    cov : (N, N) array or sparse matrix
        Covariance matrix.

    use_cholesky : bool, optional
        Whether to use the Cholesky decomposition or eigenvalue decomposition.
        The former is faster but fails when `cov` is not numerically positive
        definite.

    count : int, optional
        Number of samples to draw.

    Returns
    -------
    (N,) or (count, N) array

    '''
    mu = np.asarray(mu)
    assert_shape(mu, (None, ), 'mu')
    n = mu.shape[0]

    cov = as_sparse_or_array(cov)
    assert_shape(cov, (n, n), 'cov')

    if use_cholesky:
        # draw a sample using a cholesky decomposition. This assumes that `cov`
        # is numerically positive definite (i.e. no small negative eigenvalues
        # from rounding error).
        L = PosDefSolver(cov).L()
        if count is None:
            w = np.random.normal(0.0, 1.0, n)
            u = mu + L.dot(w)
        else:
            w = np.random.normal(0.0, 1.0, (n, count))
            u = (mu[:, None] + L.dot(w)).T

    else:
        # otherwise use an eigenvalue decomposition, ignoring negative
        # eigenvalues. If `cov` is sparse then begrudgingly make it dense.
        cov = as_array(cov)
        vals, vecs = np.linalg.eigh(cov)
        keep = (vals > 0.0)
        vals = np.sqrt(vals[keep])
        vecs = vecs[:, keep]
        if count is None:
            w = np.random.normal(0.0, vals)
            u = mu + vecs.dot(w)
        else:
            w = np.random.normal(0.0, vals[:, None].repeat(count, axis=1))
            u = (mu[:, None] + vecs.dot(w)).T

    return u
Example #4
0
    def basis(self, x, diff=None):
        '''
        Returns the basis functions evaluated at `x`.

        Parameters
        ----------
        x : (N, D) array
            Evaluation points.

        diff : (D,) int array
            Derivative specification.

        Returns
        -------
        (N, P) array

        '''
        x = np.asarray(x, dtype=float)
        assert_shape(x, (None, self.dim), 'x')
        dim = x.shape[1]

        if diff is None:
            diff = np.zeros(dim, dtype=int)
        else:
            diff = np.asarray(diff, dtype=int)
            assert_shape(diff, (dim, ), 'diff')

        if self._basis is None:
            out = empty_basis(x, diff)
        else:
            out = self._basis(x, diff)

        return out
Example #5
0
    def __call__(self, x, chunk_size=100):
        '''
        Returns the mean and standard deviation of the Gaussian process.

        Parameters
        ----------
        x : (N, D) array
            Evaluation points.

        chunk_size : int, optional
            Break `x` into chunks with this size for evaluation.

        Returns
        -------
        (N,) array
            Mean at `x`.

        (N,) array
            One standard deviation at `x`.

        '''
        x = np.asarray(x, dtype=float)
        assert_shape(x, (None, self.dim), 'x')
        n, dim = x.shape

        diff = np.zeros(dim, dtype=int)

        out_mu = np.empty(n, dtype=float)
        out_sigma = np.empty(n, dtype=float)
        for start in range(0, n, chunk_size):
            stop = start + chunk_size
            out_mu[start:stop] = self.mean(x[start:stop], diff)
            out_sigma[start:stop] = np.sqrt(self.variance(x[start:stop], diff))

        return out_mu, out_sigma
Example #6
0
    def mean(self, x, diff=None):
        '''
        Returns the mean of the Gaussian process.

        Parameters
        ----------
        x : (N, D) array
            Evaluation points.

        diff : (D,) int array
            Derivative specification.

        Returns
        -------
        (N,) array

        '''
        x = np.asarray(x, dtype=float)
        assert_shape(x, (None, self.dim), 'x')
        dim = x.shape[1]

        if diff is None:
            diff = np.zeros(dim, dtype=int)
        else:
            diff = np.asarray(diff, dtype=int)
            assert_shape(diff, (dim, ), 'diff')

        if self._mean is None:
            out = zero_mean(x, diff)
        else:
            out = self._mean(x, diff)

        return out
Example #7
0
    def __init__(self, vertices, simplices):
        vertices = np.asarray(vertices, dtype=float)
        simplices = np.asarray(simplices, dtype=int)
        assert_shape(vertices, (None, None), 'vertices')
        dim = vertices.shape[1]
        assert_shape(simplices, (None, dim), 'simplices')

        self.vertices = vertices
        self.simplices = simplices
        self.dim = dim     
        self.rtree = None
        self.normals = geo.simplex_normals(vertices, simplices)
Example #8
0
    def intersection_count(self, start_points, end_points):
        '''
        Counts the number times the line segments intersect the
        boundary.

        Parameters
        ----------
        start_points, end_points : (n, d) float array
            The ends of the line segments

        Returns
        -------
        (n,) int array
            The number of boundary intersection

        '''
        start_points = np.asarray(start_points, dtype=float)
        end_points = np.asarray(end_points, dtype=float)
        assert_shape(start_points, (None, self.dim), 'start_points')
        assert_shape(end_points, start_points.shape, 'end_points')
        n = start_points.shape[0]
        
        if self.rtree is None:
            return geo.intersection_count(
                start_points,
                end_points,
                self.vertices,
                self.simplices)

        else:
            out = np.zeros(n, dtype=int)
            # get the bounding boxes around each segment
            bounds = np.hstack((np.minimum(start_points, end_points),
                                np.maximum(start_points, end_points)))   
            for i, bnd in enumerate(bounds):
                # get a list of simplices which could potentially be
                # intersected by segment i
                potential_smpid = list(self.rtree.intersection(bnd))
                if not potential_smpid:
                    # if the segment bounding box does not intersect
                    # and simplex bounding boxes, then there is no
                    # intersection
                    continue
                
                out[[i]] = geo.intersection_count(
                    start_points[[i]],
                    end_points[[i]],
                    self.vertices,
                    self.simplices[potential_smpid])

            return out                    
Example #9
0
def neighbor_argsort(nodes, m=None):
  '''
  Returns a permutation array that sorts `nodes` so that each node and
  its `m` nearest neighbors are close together in memory. This is done
  through the use of a KD Tree and the Reverse Cuthill-McKee
  algorithm.

  Parameters
  ----------
  nodes : (n, d) float array
  
  m : int, optional
         
  Returns
  -------
  (N,) int array

  Examples
  --------
  >>> nodes = np.array([[0.0, 1.0],
                        [2.0, 1.0],
                        [1.0, 1.0]])
  >>> idx = neighbor_argsort(nodes, 2)
  >>> nodes[idx]
  array([[ 2.,  1.],
         [ 1.,  1.],
         [ 0.,  1.]])

  '''
  nodes = np.asarray(nodes, dtype=float)
  assert_shape(nodes, (None, None), 'nodes')
  
  if m is None:
    # this should be roughly equal to the stencil size for the RBF-FD
    # problem
    m = 5**nodes.shape[1]

  m = min(m, nodes.shape[0])
  # find the indices of the nearest m nodes for each node
  _, idx = KDTree(nodes).query(nodes, m)
  # efficiently form adjacency matrix
  col = idx.ravel()
  row = np.repeat(np.arange(nodes.shape[0]), m)
  data = np.ones(nodes.shape[0]*m, dtype=bool)
  mat = csc_matrix((data, (row, col)), dtype=bool)
  permutation = reverse_cuthill_mckee(mat)
  return permutation
Example #10
0
    def __call__(self, x, diff=None, chunk_size=1000):
        ''' 
    Evaluates the interpolant at `x`

    Parameters 
    ---------- 
    x : (N, D) array
      Target points.

    diff : (D,) int array, optional
      Derivative order for each spatial dimension.
        
    chunk_size : int, optional  
      Break `x` into chunks with this size and evaluate the
      interpolant for each chunk.  Smaller values result in decreased
      memory usage but also decreased speed.

    Returns
    -------
    out : (N,) array
      Values of the interpolant at `x`
      
    '''
        x = np.asarray(x, dtype=float)
        assert_shape(x, (None, self._y.shape[1]), 'x')

        xlen = x.shape[0]
        # allocate output array
        out = np.zeros(xlen, dtype=float)
        count = 0
        while count < xlen:
            start, stop = count, count + chunk_size
            K = self._phi(x[start:stop], self._y, eps=self._eps, diff=diff)
            P = mvmonos(x[start:stop], self._pwr, diff=diff)
            out[start:stop] = (K.dot(self._phi_coeff) +
                               P.dot(self._poly_coeff))
            count += chunk_size

        # return zero for points outside of the convex hull if
        # extrapolation is not allowed
        if not self.extrapolate:
            out[~_in_hull(x, self._y)] = np.nan

        return out
Example #11
0
    def log_likelihood(self, y, d, dcov=None, dvecs=None):
        '''
        Returns the log likelihood of drawing the observations `d` from the
        Gaussian process. The observations could potentially have noise which
        is described by `dcov` and `dvecs`. If the Gaussian process contains
        any basis functions or if `dvecs` is specified, then the restricted
        log likelihood is returned.

        Parameters
        ----------
        y : (N, D) array
            Observation points.

        d : (N,) array
            Observed values at `y`.

        dcov : (N, N) array or sparse matrix, optional
            Data covariance. If not given, this will be a dense matrix of
            zeros.

        dvecs : (N, P) float array, optional
            Basis vectors for the noise. The data noise is assumed to contain
            some unknown linear combination of the columns of `dvecs`.

        Returns
        -------
        float

        '''
        y = np.asarray(y, dtype=float)
        assert_shape(y, (None, self.dim), 'y')
        n, dim = y.shape

        d = np.asarray(d, dtype=float)
        assert_shape(d, (n, ), 'd')

        if dcov is None:
            dcov = np.zeros((n, n), dtype=float)
        else:
            dcov = as_sparse_or_array(dcov)
            assert_shape(dcov, (n, n), 'dcov')

        if dvecs is None:
            dvecs = np.zeros((n, 0), dtype=float)
        else:
            dvecs = np.asarray(dvecs, dtype=float)
            assert_shape(dvecs, (n, None), 'dvecs')

        mu = self.mean(y)
        cov = as_sparse_or_array(dcov + self.covariance(y, y))
        vecs = np.hstack((self.basis(y), dvecs))

        out = log_likelihood(d, mu, cov, vecs=vecs)
        return out
Example #12
0
    def contains(self, points):
        '''
        Identifies whether the points are within the domain

        Parameters
        ----------
        points : (n, d) float array

        Returns
        -------
        (n,) bool array
        
        '''
        points = np.asarray(points, dtype=float)
        assert_shape(points, (None, self.dim), 'points')
        # to find out if the points are inside the domain, we create
        # another set of points which are definitively outside the
        # domain, and then we count the number of boundary
        # intersections between `points` and the new points.

        # get the min value and width of the domain along axis 0
        xwidth = self.vertices[:, 0].ptp()
        xmin = self.vertices[:, 0].min()
        # the outside points are directly to the left of `points` plus
        # a small random perturbation. The subsequent bounding boxes
        # are going to be very narrow, meaning that the R-tree will
        # efficiently winnow down the potential intersecting
        # simplices.
        outside_points = np.array(points, copy=True)
        outside_points[:, 0] = xmin - xwidth
        outside_points += np.random.uniform(
            -0.001*xwidth, 
            0.001*xwidth,
            points.shape)
        count = self.intersection_count(points, outside_points)            
        # If the segment intersects the boundary an odd number of
        # times, then the point is inside the domain, otherwise it is
        # outside
        out = np.array(count % 2, dtype=bool)
        return out
Example #13
0
    def __call__(self, x, diff=None, chunk_size=1000):
        '''
    Evaluates the interpolant at `x`

    Parameters
    ----------
    x : (N, D) float array
      Target points

    diff : (D,) int array, optional
      Derivative order for each spatial dimension

    chunk_size : int, optional
      Break `x` into chunks with this size and evaluate the interpolant for
      each chunk

    Returns
    -------
    (N,) float array

    '''
        x = np.asarray(x, dtype=float)
        assert_shape(x, (None, self.y.shape[1]), 'x')
        nx = x.shape[0]

        if chunk_size is not None:
            out = np.zeros(nx, dtype=float)
            for start in range(0, nx, chunk_size):
                stop = start + chunk_size
                out[start:stop] = self(x[start:stop],
                                       diff=diff,
                                       chunk_size=None)

            return out

        x = x - self.center
        Kxy = self.phi(x, self.y, eps=self.eps, diff=diff)
        Px = mvmonos(x, self.order, diff=diff)
        out = Kxy.dot(self.phi_coeff) + Px.dot(self.poly_coeff)
        return out
Example #14
0
    def __init__(self, y, d, sigma=0.0, phi='phs3', eps=1.0, order=None):
        y = np.asarray(y, dtype=float)
        assert_shape(y, (None, None), 'y')
        ny, ndim = y.shape

        d = np.asarray(d, dtype=float)
        assert_shape(d, (ny, ), 'd')

        if np.isscalar(sigma):
            sigma = np.full(ny, sigma, dtype=float)
        else:
            sigma = np.asarray(sigma, dtype=float)
            assert_shape(sigma, (ny, ), 'sigma')

        phi = get_rbf(phi)

        if not np.isscalar(eps):
            raise ValueError('The shape parameter should be a float')

        # If `phi` is not in `_MIN_ORDER`, then the RBF is either positive definite
        # (no minimum polynomial order) or user-defined
        min_order = _MIN_ORDER.get(phi, -1)
        if order is None:
            order = max(min_order, 0)
        elif order < min_order:
            logger.warning(
                'The polynomial order should not be below %d for %s in order for the '
                'interpolant to be well-posed' % (min_order, phi))

        order = int(order)
        # For improved numerical stability, shift the observations so that their
        # centroid is at zero
        center = y.mean(axis=0)
        y = y - center
        # Build the system of equations and solve for the RBF and mononomial
        # coefficients
        Kyy = phi(y, y, eps=eps)
        S = scipy.sparse.diags(sigma**2)
        Py = mvmonos(y, order)
        nmonos = Py.shape[1]
        if nmonos > ny:
            raise ValueError(
                'The polynomial order is too high. The number of monomials, %d, '
                'exceeds the number of observations, %d' % (nmonos, ny))

        z = np.zeros(nmonos, dtype=float)
        phi_coeff, poly_coeff = PartitionedSolver(Kyy + S, Py).solve(d, z)

        self.y = y
        self.phi = phi
        self.eps = eps
        self.order = order
        self.center = center
        self.phi_coeff = phi_coeff
        self.poly_coeff = poly_coeff
Example #15
0
    def covariance(self, x1, x2, diff1=None, diff2=None):
        '''
        Returns the covariance matrix of the Gaussian process.

        Parameters
        ----------
        x1, x2 : (N, D) and (M, D) array
            Evaluation points.

        diff1, diff2 : (D,) int array
            Derivative specifications.

        Returns
        -------
        (N, M) array or sparse matrix

        '''
        x1 = np.asarray(x1, dtype=float)
        assert_shape(x1, (None, self.dim), 'x1')
        dim = x1.shape[1]

        x2 = np.asarray(x2, dtype=float)
        assert_shape(x2, (None, dim), 'x2')

        if diff1 is None:
            diff1 = np.zeros(dim, dtype=int)
        else:
            diff1 = np.asarray(diff1, dtype=int)
            assert_shape(diff1, (dim, ), 'diff1')

        if diff2 is None:
            diff2 = np.zeros(dim, dtype=int)
        else:
            diff2 = np.asarray(diff2, dtype=int)
            assert_shape(diff2, (dim, ), 'diff2')

        if self._covariance is None:
            out = zero_covariance(x1, x2, diff1, diff2)
        else:
            out = self._covariance(x1, x2, diff1, diff2)

        return out
Example #16
0
def _sanitize_arguments(y, d, sigma, phi, eps, order, k=None):
    '''Sanitize input to RBFInterpolant and KNearestRBFInterpolant'''
    y = np.asarray(y, dtype=float)
    assert_shape(y, (None, None), 'y')
    ny, ndim = y.shape

    d = np.asarray(d, dtype=float)
    assert_shape(d, (ny, ), 'd')

    if np.isscalar(sigma):
        sigma = np.full(ny, sigma, dtype=float)
    else:
        sigma = np.asarray(sigma, dtype=float)
        assert_shape(sigma, (ny, ), 'sigma')

    phi = get_rbf(phi)

    if not np.isscalar(eps):
        raise ValueError('`eps` should be a scalar.')

    # If `phi` is not in `_MIN_ORDER`, then the RBF is either positive definite
    # (no minimum polynomial order) or user-defined (no known minimum
    # polynomial order)
    min_order = _MIN_ORDER.get(phi, -1)
    if order is None:
        order = max(min_order, 0)
    else:
        order = int(order)
        if order < -1:
            raise ValueError('`order` must be at least -1.')

        elif order < min_order:
            logger.warning(
                'The polynomial order should not be below %d when `phi` is '
                '%s. The interpolant may not be well-posed.' %
                (min_order, phi))

    nmonos = monomial_count(order, ndim)
    if k is None:
        nobs = ny
    else:
        # make sure the number of neighbors does not exceed the number of
        # observations.
        k = int(min(k, ny))
        nobs = k

    if nmonos > nobs:
        raise ValueError(
            'At least %d data points are required when `order` is %d and the '
            'number of dimensions is %d' % (nmonos, order, ndim))

    return y, d, sigma, phi, eps, order, k
Example #17
0
    def __init__(self, y, d, sigma=0.0, k=20, phi='phs3', eps=1.0, order=None):
        y = np.asarray(y, dtype=float)
        assert_shape(y, (None, None), 'y')
        ny, ndim = y.shape

        d = np.asarray(d, dtype=float)
        assert_shape(d, (ny, ), 'd')

        if np.isscalar(sigma):
            sigma = np.full(ny, sigma, dtype=float)
        else:
            sigma = np.asarray(sigma, dtype=float)
            assert_shape(sigma, (ny, ), 'sigma')

        # make sure the number of nearest neighbors used for interpolation does not
        # exceed the number of observations
        k = min(int(k), ny)

        phi = get_rbf(phi)
        if isinstance(phi, SparseRBF):
            raise ValueError('SparseRBF instances are not supported')

        if not np.isscalar(eps):
            raise ValueError('The shape parameter should be a float')

        min_order = _MIN_ORDER.get(phi, -1)
        if order is None:
            order = max(min_order, 0)
        elif order < min_order:
            logger.warning(
                'The polynomial order should not be below %d for %s in order for the '
                'interpolant to be well-posed' % (min_order, phi))

        order = int(order)
        nmonos = monomial_count(order, ndim)
        if nmonos > k:
            raise ValueError(
                'The polynomial order is too high. The number of monomials, %d, '
                'exceeds the number of neighbors used for interpolation, %d' %
                (nmonos, k))

        tree = KDTree(y)

        self.y = y
        self.d = d
        self.sigma = sigma
        self.k = k
        self.eps = eps
        self.phi = phi
        self.order = order
        self.tree = tree
Example #18
0
    def outliers(self, x, d, dsigma, tol=4.0, maxitr=50):
        '''
        Identifies values in `d` that are abnormally inconsistent with the the
        Gaussian process

        Parameters
        ----------
        x : (N, D) float array
            Observations locations.

        d : (N,) float array
            Observations.

        dsigma : (N,) float array
            One standard deviation uncertainty on the observations.

        tol : float, optional
            Outlier tolerance. Smaller values make the algorithm more likely to
            identify outliers. A good value is 4.0 and this should not be set
            any lower than 2.0.

        maxitr : int, optional
            Maximum number of iterations.

        Returns
        -------
        (N,) bool array
            Array indicating which data are outliers

        '''
        x = np.asarray(x, dtype=float)
        assert_shape(x, (None, self.dim), 'x')
        n, dim = x.shape

        d = np.asarray(d, dtype=float)
        assert_shape(d, (n, ), 'd')

        dsigma = np.asarray(dsigma, dtype=float)
        assert_shape(dsigma, (n, ), 'dsigma')

        pcov = self.covariance(x, x)
        pmu = self.mean(x)
        pvecs = self.basis(x)
        out = outliers(d,
                       dsigma,
                       pcov,
                       pmu=pmu,
                       pvecs=pvecs,
                       tol=tol,
                       maxitr=maxitr)
        return out
Example #19
0
    def __init__(self,
                 y,
                 d,
                 sigma=None,
                 eps=1.0,
                 phi=phs3,
                 order=1,
                 extrapolate=True):
        y = np.asarray(y)
        assert_shape(y, (None, None), 'y')
        nobs, dim = y.shape

        d = np.asarray(d)
        assert_shape(d, (nobs, ), 'd')

        if sigma is None:
            # if sigma is not specified then it is zeros
            sigma = np.zeros(nobs)

        elif np.isscalar(sigma):
            # if a float is specified then use it as the uncertainties for
            # all observations
            sigma = np.repeat(sigma, nobs)

        else:
            sigma = np.asarray(sigma)
            assert_shape(sigma, (nobs, ), 'sigma')

        phi = get_rbf(phi)

        # form block consisting of the RBF and uncertainties on the
        # diagonal
        K = phi(y, y, eps=eps)
        Cd = scipy.sparse.diags(sigma**2)
        # form the block consisting of the monomials
        pwr = powers(order, dim)
        P = mvmonos(y, pwr)
        # create zeros vector for the right-hand-side
        z = np.zeros((pwr.shape[0], ))
        # solve for the RBF and mononomial coefficients
        phi_coeff, poly_coeff = PartitionedSolver(K + Cd, P).solve(d, z)

        self._y = y
        self._phi = phi
        self._order = order
        self._eps = eps
        self._phi_coeff = phi_coeff
        self._poly_coeff = poly_coeff
        self._pwr = pwr
        self.extrapolate = extrapolate
Example #20
0
File: nodes.py Project: NelisW/RBF
def disperse(nodes, domain,
             iterations=20,
             rho=None,
             fixed_nodes=None,
             neighbors=None,
             delta=0.1):
    '''
    Disperses the nodes within the domain. The dispersion is analogous to
    electrostatic repulsion, where neighboring nodes exert a repulsive force on
    eachother. Each node steps in the direction of its net repulsive force with
    a step size proportional to the distance to its nearest neighbor. If a node
    is repelled into a boundary then it bounces back in.

    Parameters
    ----------
    nodes : (n, d) float array
        Initial node positions

    domain : (p, d) float array and (q, d) int array
        Vertices of the domain and connectivity of the vertices.

    iterations : int, optional
        Number of dispersion iterations.

    rho : callable, optional
        Takes an (n, d) array as input and returns the repulsion force for a
        node at those position.

    fixed_nodes : (k, d) float array, optional
        Nodes which do not move and only provide a repulsion force.

    neighbors : int, optional
        The number of adjacent nodes used to determine repulsion forces for
        each node.

    delta : float, optional
        The step size. Each node moves in the direction of the repulsion force
        by a distance `delta` times the distance to the nearest neighbor.

    Returns
    -------
    (n, d) float array

    '''
    domain = as_domain(domain)
    nodes = np.asarray(nodes, dtype=float)
    assert_shape(nodes, (None, domain.dim), 'nodes')

    if rho is None:
        def rho(x):
            return np.ones(x.shape[0])

    if fixed_nodes is None:
        fixed_nodes = np.zeros((0, domain.dim), dtype=float)
    else:
        fixed_nodes = np.asarray(fixed_nodes)
        assert_shape(fixed_nodes, (None, domain.dim), 'fixed_nodes')

    if neighbors is None:
        # the default number of neighboring nodes to use when computing the
        # repulsion force is 3 for 2D and 4 for 3D
        if domain.dim == 2:
            neighbors = 3

        elif domain.dim == 3:
            neighbors = 4

    # ensure that the number of neighboring nodes used for the repulsion force
    # is less than or equal to the total number of nodes
    neighbors = min(neighbors, nodes.shape[0] + fixed_nodes.shape[0] - 1)

    for itr in range(iterations):
        logger.debug(
            'Starting node dispersion iterations %s of %s.'
            % (itr + 1, iterations)
            )

        new_nodes = _disperse_step(nodes, rho, fixed_nodes, neighbors, delta)
        # If the line segment connecting the new and old node crosses the
        # boundary, then the node should bounce off the boundary.
        crossed, = domain.intersection_count(nodes, new_nodes).nonzero()
        # points where nodes intersected the boundary and the simplex they
        # intersected at
        intr_pnt, intr_idx = domain.intersection_point(
            nodes[crossed],
            new_nodes[crossed]
            )

        # normal vector to the intersection points
        intr_norms = domain.normals[intr_idx]
        # residual distance that the nodes wanted to travel beyond the boundary
        res = new_nodes[crossed] - intr_pnt
        # normal component of the residuals
        res_perp = np.sum(res*intr_norms, axis=1)
        # bounce nodes off the boundary
        new_nodes[crossed] -= 2*intr_norms*res_perp[:, None]
        # check to see if the bounced nodes are still crossing the boundary. If
        # they are, then set them back to their original position. Do not
        # bother with multiple bounces.
        still_crossed, = domain.intersection_count(
            nodes[crossed],
            new_nodes[crossed]
            ).nonzero()

        new_nodes[crossed[still_crossed]] = nodes[crossed[still_crossed]]
        nodes = new_nodes

    return nodes
Example #21
0
def stencil_network(x, p, n, vert=None, smp=None):
    ''' 
  Forms a stencil for each point in `x`. Each stencil is made up of 
  `n` nearby points from `p`. Stencils can be constrained to not 
  intersect a boundary defined by `vert` and `smp`.
  
  Parameters
  ----------
  x : (N, D) array
    Target points. A stencil will be made for each point in `x`.

  p : (M, D) array
    Source points. The stencils will be made up of points from `p`.

  n : int
    Stencil size.

  vert : (P, D) array, optional
    Vertices of the boundary which stencils cannot intersect.

  smp : (Q, D) array, optional
    Connectivity of the vertices to form the boundary.

  Returns
  -------
  sn : (N, D) array
    Indices of points in `p` which form a stencil for each point in 
    `x`.
    
  '''
    x = np.asarray(x, dtype=float)
    assert_shape(x, (None, None), 'x')

    p = np.asarray(p, dtype=float)
    assert_shape(p, (None, x.shape[1]), 'p')

    Nx = x.shape[0]
    Np = p.shape[0]
    if n > Np:
        raise StencilError('cannot form a stencil with size %s from %s nodes' %
                           (n, Np))

    if (vert is None) | (smp is None):
        vert = np.zeros((0, x.shape[1]), dtype=float)
        smp = np.zeros((0, x.shape[1]), dtype=int)

    else:
        vert = np.asarray(vert, dtype=float)
        assert_shape(vert, (None, x.shape[1]), 'vert')

        smp = np.asarray(smp, dtype=int)
        assert_shape(smp, (None, x.shape[1]), 'smp')

    sn = _stencil_network_no_boundary(x, p, n)
    if smp.shape[0] == 0:
        return sn

    # ensure that no stencils intersect the boundary
    for i in range(Nx):
        if _has_intersections(x[i], p[sn[i]], vert, smp):
            sn[i, :] = _stencil(x[i], p, n, vert, smp)

    return sn
Example #22
0
    def __call__(self, x, c, eps=1.0, diff=None):
        ''' 
    Numerically evaluates the RBF or its derivatives.
    
    Parameters                                       
    ----------                                         
    x : (N, D) float array 
      Evaluation points
                                                                       
    c : (M, D) float array 
      RBF centers 
        
    eps : float, optional
      Shape parameter
                                                                           
    diff : (D,) int array, optional
      Specifies the derivative order for each Cartesian direction. For
      example, if there are three spatial dimensions then providing
      (2, 0, 1) would cause this function to return the RBF after
      differentiating it twice along the first axis and once along the
      third axis.

    Returns
    -------
    out : (N, M) csc sparse matrix
      The RBFs with centers `c` evaluated at `x`
      
    '''
        x = np.asarray(x, dtype=float)
        assert_shape(x, (None, None), 'x')

        c = np.asarray(c, dtype=float)
        assert_shape(c, (None, x.shape[1]), 'c')

        if not np.isscalar(eps):
            raise NotImplementedError(
                '`eps` must be a scalar for `SparseRBF` instances')

        # convert scalar to (1,) array
        eps = np.array([eps], dtype=float)

        if diff is None:
            diff = (0, ) * x.shape[1]

        else:
            # make sure diff is immutable
            diff = tuple(diff)
            assert_shape(diff, (x.shape[1], ), 'diff')

        # add numerical function to cache if not already
        if diff not in self._cache:
            self._add_diff_to_cache(diff)

        # convert self.supp from a sympy expression to a float
        supp = float(self.supp.subs(_EPS, eps[0]))

        # find the nonzero entries based on distances between `x` and `c`
        nx, nc = x.shape[0], c.shape[0]
        xtree = cKDTree(x)
        ctree = cKDTree(c)
        # `idx` contains the indices of `x` which are within
        # `supp` of each node in `c`
        idx = ctree.query_ball_tree(xtree, supp)

        # total nonzero entries in the output array
        nnz = sum(len(i) for i in idx)
        # allocate sparse matrix data
        data = np.zeros(nnz, dtype=float)
        rows = np.zeros(nnz, dtype=int)
        cols = np.zeros(nnz, dtype=int)
        # `n` is the total number of data entries thus far
        n = 0
        for i, idxi in enumerate(idx):
            # `m` is the number of nodes in `x` close to `c[[i]]`
            m = len(idxi)
            # properly shape `x` and `c` for broadcasting
            xi = x.T[:, idxi, None]
            ci = c.T[:, None, i][:, :, None]
            args = (tuple(xi) + tuple(ci) + (eps, ))
            data[n:n + m] = self._cache[diff](*args)[:, 0]
            rows[n:n + m] = idxi
            cols[n:n + m] = i
            n += m

        # convert to a csc_matrix
        out = csc_matrix((data, (rows, cols)), (nx, nc))
        return out
Example #23
0
    def __call__(self, x, c, eps=1.0, diff=None):
        ''' 
    Numerically evaluates the RBF or its derivatives.
    
    Parameters                                       
    ----------                                         
    x : (N, D) float array 
      Evaluation points
                                                                       
    c : (M, D) float array 
      RBF centers 
        
    eps : float or (M,) float array, optional
      Shape parameters for each RBF. Defaults to 1.0
                                                                           
    diff : (D,) int array, optional
      Specifies the derivative order for each spatial dimension. For
      example, if there are three spatial dimensions then providing
      (2, 0, 1) would cause this function to return the RBF after
      differentiating it twice along the first dimension and once
      along the third dimension.

    Returns
    -------
    (N, M) float array
      The RBFs with centers `c` evaluated at `x`

    '''
        x = np.asarray(x, dtype=float)
        assert_shape(x, (None, None), 'x')

        c = np.asarray(c, dtype=float)
        assert_shape(c, (None, x.shape[1]), 'c')

        # makes `eps` an array of constant values if it is a scalar
        if np.isscalar(eps):
            eps = np.full(c.shape[0], eps, dtype=float)

        else:
            eps = np.asarray(eps, dtype=float)
            assert_shape(eps, (c.shape[0], ), 'eps')

        # if `diff` is not given then take no derivatives
        if diff is None:
            diff = (0, ) * x.shape[1]

        else:
            # make sure diff is immutable
            diff = tuple(diff)
            assert_shape(diff, (x.shape[1], ), 'diff')

        # add numerical function to cache if not already
        if diff not in self._cache:
            self._add_diff_to_cache(diff)

        # expand to allow for broadcasting
        x = x.T[:, :, None]
        c = c.T[:, None, :]
        args = (tuple(x) + tuple(c) + (eps, ))
        # evaluate the cached function for the given `x`, `c`, and `eps
        out = self._cache[diff](*args)
        return out
Example #24
0
    def __call__(self, x, c, eps=1.0, diff=None):
        '''
        Numerically evaluates the RBF or its derivatives.

        Parameters
        ----------
        x : (..., N, D) float array
            Evaluation points

        c : (..., M, D) float array
            RBF centers

        eps : float or float array, optional
            Shape parameter for each RBF

        diff : (D,) int array, optional
            Specifies the derivative order for each spatial dimension. For
            example, if there are three spatial dimensions then providing
            (2, 0, 1) would cause this function to return the RBF after
            differentiating it twice along the first dimension and once along
            the third dimension.

        Returns
        -------
        (..., N, M) float array
            The RBFs with centers `c` evaluated at `x`

        Notes
        -----
        The default method for converting the symbolic RBF to a numeric
        function limits the number of spatial dimensions `D` to 15. There is no
        such limitation when the conversion method is set to "lambdify". Set
        the conversion method using the function
        `set_symbolic_to_numeric_method`.

        The derivative order can be arbitrarily high, but some RBFs, such as
        Wendland and Matern, become numerically unstable when the derivative
        order exceeds 2.

        '''
        x = np.asarray(x, dtype=float)
        assert_shape(x, (..., None, None), 'x')
        ndim = x.shape[-1]

        c = np.asarray(c, dtype=float)
        assert_shape(c, (..., None, ndim), 'c')

        eps = np.asarray(eps, dtype=float)
        eps = np.broadcast_to(eps, c.shape[:-1])

        # if `diff` is not given then take no derivatives
        if diff is None:
            diff = (0,)*ndim

        else:
            # make sure diff is immutable
            diff = tuple(diff)
            assert_shape(diff, (ndim,), 'diff')

        # add numerical function to cache if not already
        if diff not in self._cache:
            self._add_diff_to_cache(diff)

        # reshape x from (..., n, d) to (d, ..., n, 1)
        x = np.einsum('...ij->j...i', x)[..., None]
        # reshape c from (..., m, d) to (d, ..., 1, m)
        c = np.einsum('...ij->j...i', c)[..., None, :]
        # reshape eps from (..., m) to (..., 1, m)
        eps = eps[..., None, :]
        args = (tuple(x) + tuple(c) + (eps,))
        # evaluate the cached function for the given `x`, `c`, and `eps`
        out = self._cache[diff](*args)
        return out
Example #25
0
File: nodes.py Project: NelisW/RBF
def prepare_nodes(nodes, domain,
                  rho=None,
                  iterations=20,
                  neighbors=None,
                  dispersion_delta=0.1,
                  pinned_nodes=None,
                  snap_delta=0.5,
                  boundary_groups=None,
                  boundary_groups_with_ghosts=None,
                  ghost_delta=0.5,
                  include_vertices=False,
                  orient_simplices=True):
    '''
    Prepares a set of nodes for solving PDEs with the RBF and RBF-FD method.
    This includes: dispersing the nodes away from eachother to ensure a more
    even spacing, snapping nodes to the boundary, determining the normal
    vectors for each node, determining the group that each node belongs to,
    creating ghost nodes, sorting the nodes so that adjacent nodes are close in
    memory, and verifying that no two nodes are anomalously close to eachother.

    The function returns a set of nodes, the normal vectors for each node, and
    a dictionary identifying which group each node belongs to.

    Parameters
    ----------
    nodes : (n, d) float arrary
        An initial sampling of nodes within the domain

    domain : (p, d) float array and (q, d) int array
        Vertices of the domain and connectivity of the vertices

    rho : function, optional
        Node density function. Takes a (n, d) array of coordinates and returns
        an (n,) array of desired node densities at those coordinates. This is
        used during the node dispersion step.

    iterations : int, optional
        Number of dispersion iterations.

    neighbors : int, optional
        Number of neighboring nodes to use when calculating the repulsion
        force. This defaults to 3 for 2D nodes and 4 for 3D nodes.

    dispersion_delta : float, optional
        Scaling factor for the node step size in each iteration. The step size
        is equal to `dispersion_delta` times the distance to the nearest
        neighbor.

    pinned_nodes : (k, d) array, optional
        Nodes which do not move and only provide a repulsion force. These nodes
        are included in the set of nodes returned by this function and they are
        in the group named "pinned".

    snap_delta : float, optional
        Controls the maximum snapping distance. The maximum snapping distance
        for each node is `snap_delta` times the distance to the nearest
        neighbor. This defaults to 0.5.

    boundary_groups: dict, optional
        Dictionary defining the boundary groups. The keys are the names of the
        groups and the values are lists of simplex indices making up each
        group. This function will return a dictionary identifying which nodes
        belong to each boundary group. By default, there is a single group
        named 'all' for the entire boundary. Specifically, The default value is
        `{'all':range(len(smp))}`.

    boundary_groups_with_ghosts: list of strs, optional
        List of boundary groups that will be given ghost nodes. By default, no
        boundary groups are given ghost nodes. The groups specified here must
        exist in `boundary_groups`.

    ghost_delta : float, optional
        How far the ghost nodes should be from their corresponding boundary
        node. The distance is `ghost_delta` times the distance to the nearest
        neighbor.

    include_vertices : bool, optional
        If `True`, then the vertices will be included in the output nodes. Each
        vertex will be assigned to the boundary group that its adjoining
        simplices are part of. If the simplices are in multiple groups, then
        the vertex will be assigned to the group containing the simplex that
        comes first in `smp`.

    orient_simplices : bool, optional
        If `False` then it is assumed that the simplices are already oriented
        such that their normal vectors point outward.

    Returns
    -------
    (m, d) float array
        Nodes positions

    dict
        The indices of nodes belonging to each group. There will always be a
        group called 'interior' containing the nodes that are not on the
        boundary. By default there is a group containing all the boundary nodes
        called 'boundary:all'. If `boundary_groups` was specified, then those
        groups will be included in this dictionary and their names will be
        given a 'boundary:' prefix. If `boundary_groups_with_ghosts` was
        specified then those groups of ghost nodes will be included in this
        dictionary and their names will be given a 'ghosts:' prefix.

    (n, d) float array
        Outward normal vectors for each node. If a node is not on the boundary
        then its corresponding row will contain NaNs.

    '''
    domain = as_domain(domain)
    if orient_simplices:
        logger.debug('Orienting simplices...')
        domain.orient_simplices()
        logger.debug('Done')

    nodes = np.asarray(nodes, dtype=float)
    assert_shape(nodes, (None, domain.dim), 'nodes')

    # the `fixed_nodes` are used to provide a repulsion force during
    # dispersion, but they do not move.
    fixed_nodes = np.zeros((0, domain.dim), dtype=float)
    if pinned_nodes is not None:
        pinned_nodes = np.asarray(pinned_nodes, dtype=float)
        assert_shape(pinned_nodes, (None, domain.dim), 'pinned_nodes')
        fixed_nodes = np.vstack((fixed_nodes, pinned_nodes))

    if include_vertices:
        fixed_nodes = np.vstack((fixed_nodes, domain.vertices))

    logger.debug('Dispersing nodes...')
    nodes = disperse(
        nodes, domain,
        iterations=iterations,
        rho=rho,
        fixed_nodes=fixed_nodes,
        neighbors=neighbors,
        delta=dispersion_delta
        )

    logger.debug('Done')

    # append the domain vertices to the collection of nodes if requested
    if include_vertices:
        nodes = np.vstack((nodes, domain.vertices))

    # snap nodes to the boundary, identifying which simplex each node
    # was snapped to
    logger.debug('Snapping nodes to boundary...')
    nodes, smpid = domain.snap(nodes, delta=snap_delta)
    logger.debug('Done')

    normals = np.full_like(nodes, np.nan)
    normals[smpid >= 0] = domain.normals[smpid[smpid >= 0]]

    # create a dictionary identifying which nodes belong to which group
    groups = {}
    groups['interior'], = (smpid == -1).nonzero()

    # append the user specified pinned nodes
    if pinned_nodes is not None:
        pinned_idx = np.arange(pinned_nodes.shape[0]) + nodes.shape[0]
        pinned_normals = np.full_like(pinned_nodes, np.nan)
        nodes = np.vstack((nodes, pinned_nodes))
        normals = np.vstack((normals, pinned_normals))
        groups['pinned'] = pinned_idx


    logger.debug('Grouping boundary nodes...')
    if boundary_groups is None:
        boundary_groups = {'all': np.arange(len(domain.simplices))}
    else:
        boundary_groups = {
            str(k): np.array(v, dtype=int) for k, v in boundary_groups.items()
            }

        # Validate the user-specified boundary groups
        simplex_counts = Counter(chain(*boundary_groups.values()))
        for idx in range(len(domain.simplices)):
            if simplex_counts[idx] != 1:
                logger.warning(
                    'Simplex %s is specified %s times in the boundary groups.'
                     % (idx, simplex_counts[idx])
                     )

        extra = set(simplex_counts).difference(range(len(domain.simplices)))
        if extra:
            raise ValueError(
                'The simplex indices %s were specified in the boundary groups '
                'but do not exist.' % extra
                )

    if boundary_groups_with_ghosts is None:
        boundary_groups_with_ghosts = []

    # find the mapping from simplex indices to node indices, then use
    # `boundary_groups` to find which nodes belong to each boundary group
    smp_to_nodes = [[] for _ in range(len(domain.simplices))]
    for i, j in enumerate(smpid):
        if j != -1:
            smp_to_nodes[j].append(i)

    for bnd_name, bnd_smp in boundary_groups.items():
        bnd_idx = list(chain.from_iterable(smp_to_nodes[i] for i in bnd_smp))
        groups['boundary:%s' % bnd_name] = np.array(bnd_idx, dtype=int)

    logger.debug('Done')

    logger.debug('Creating ghost nodes...')
    tree = KDTree(nodes)
    for bnd_name in boundary_groups_with_ghosts:
        bnd_idx = groups['boundary:%s' % bnd_name]
        spacing = ghost_delta*tree.query(nodes[bnd_idx], 2)[0][:, 1]
        ghost_idx = np.arange(bnd_idx.shape[0]) + nodes.shape[0]
        ghost_nodes = nodes[bnd_idx] + spacing[:, None]*normals[bnd_idx]
        ghost_normals = np.full_like(ghost_nodes, np.nan)
        nodes = np.vstack((nodes, ghost_nodes))
        normals = np.vstack((normals, ghost_normals))
        groups['ghosts:%s' % bnd_name] = ghost_idx

    logger.debug('Done')

    logger.debug('Sorting nodes...')
    sort_idx = neighbor_argsort(nodes)
    nodes = nodes[sort_idx]
    normals = normals[sort_idx]
    reverse_sort_idx = np.argsort(sort_idx)
    groups = {k: reverse_sort_idx[v] for k, v in groups.items()}
    logger.debug('Done')


    logger.debug('Checking the quality of the generated nodes...')
    _check_spacing(nodes, rho)
    logger.debug('Done')

    return nodes, groups, normals
Example #26
0
def weights(x,
            s,
            diffs,
            coeffs=None,
            basis=rbf.basis.phs3,
            order=None,
            eps=1.0):
    ''' 
  Returns the weights which map a functions values at `s` to an 
  approximation of that functions derivative at `x`. The weights are 
  computed using the RBF-FD method described in [1]. In this function 
  `x` is a single point in D-dimensional space. Use `weight_matrix` to 
  compute the weights for multiple point.

  Parameters
  ----------
  x : (D,) array
    Target point. The weights will approximate the derivative at this
    point.

  s : (N, D) array
    Stencil points. The derivative will be approximated with a
    weighted sum of the function values at this point.

  diffs : (D,) int array or (K, D) int array 
    Derivative orders for each spatial dimension. For example `[2, 0]`
    indicates that the weights should approximate the second
    derivative with respect to the first spatial dimension in
    two-dimensional space.  diffs can also be a (K, D) array, where
    each (D,) sub-array is a term in a differential operator. For
    example the two-dimensional Laplacian can be represented as
    `[[2, 0], [0, 2]]`.

  coeffs : (K,) array, optional 
    Coefficients for each term in the differential operator specified
    with `diffs`.  Defaults to an array of ones. If `diffs` was
    specified as a (D,) array then `coeffs` should be a length 1
    array.

  basis : rbf.basis.RBF, optional
    Type of RBF. Select from those available in `rbf.basis` or create
    your own.
 
  order : int, optional
    Order of the added polynomial. This defaults to the highest
    derivative order. For example, if `diffs` is `[[2, 0], [0, 1]]`, then
    order is set to 2.

  eps : float or (N,) array, optional
    Shape parameter for each RBF, which have centers `s`. This only 
    makes a difference when using RBFs that are not scale invariant. 
    All the predefined RBFs except for the odd order polyharmonic 
    splines are not scale invariant.

  Returns
  -------
  out : (N,) array
    RBF-FD weights
    
  Examples
  --------
  Calculate the weights for a one-dimensional second order derivative.

  >>> x = np.array([1.0]) 
  >>> s = np.array([[0.0], [1.0], [2.0]]) 
  >>> diff = (2,) 
  >>> weights(x, s, diff)
  array([ 1., -2., 1.])
    
  Calculate the weights for estimating an x derivative from three 
  points in a two-dimensional plane

  >>> x = np.array([0.25, 0.25])
  >>> s = np.array([[0.0, 0.0],
                    [1.0, 0.0],
                    [0.0, 1.0]])
  >>> diff = (1, 0)
  >>> weights(x, s, diff)
  array([ -1., 1., 0.])
    
  Notes
  -----
  This function may become unstable with high order polynomials (i.e.,
  `order` is high). This can be somewhat remedied by shifting the
  coordinate system so that x is zero

  References
  ----------
  [1] Fornberg, B. and N. Flyer. A Primer on Radial Basis 
  Functions with Applications to the Geosciences. SIAM, 2015.
    
  '''
    x = np.asarray(x, dtype=float)
    assert_shape(x, (None, ), 'x')

    s = np.asarray(s, dtype=float)
    assert_shape(s, (None, x.shape[0]), 's')

    diffs = np.asarray(diffs, dtype=int)
    diffs = _reshape_diffs(diffs)

    # stencil size and number of dimensions
    size, dim = s.shape
    if coeffs is None:
        coeffs = np.ones(diffs.shape[0], dtype=float)
    else:
        coeffs = np.asarray(coeffs, dtype=float)
        assert_shape(coeffs, (diffs.shape[0], ), 'coeffs')

    max_order = _max_poly_order(size, dim)
    if order is None:
        order = _default_poly_order(diffs)
        order = min(order, max_order)

    if order > max_order:
        raise ValueError('Polynomial order is too high for the stencil size')

    # get the powers for the added monomials
    powers = rbf.poly.powers(order, dim)
    # evaluate the RBF and monomials at each point in the stencil. This
    # becomes the left-hand-side
    A = basis(s, s, eps=eps)
    P = rbf.poly.mvmonos(s, powers)
    # Evaluate the RBF and monomials for each term in the differential
    # operator. This becomes the right-hand-side.
    a = coeffs[0] * basis(x[None, :], s, eps=eps, diff=diffs[0])
    p = coeffs[0] * rbf.poly.mvmonos(x[None, :], powers, diff=diffs[0])
    for c, d in zip(coeffs[1:], diffs[1:]):
        a += c * basis(x[None, :], s, eps=eps, diff=d)
        p += c * rbf.poly.mvmonos(x[None, :], powers, diff=d)

    # squeeze `a` and `p` into 1d arrays. `a` is ran through as_array
    # because it may be sparse.
    a = rbf.linalg.as_array(a)[0]
    p = p[0]

    # attempt to compute the RBF-FD weights
    try:
        w = PartitionedSolver(A, P).solve(a, p)[0]
        return w

    except np.linalg.LinAlgError:
        raise np.linalg.LinAlgError(
            'An error was raised while computing the RBF-FD weights at '
            'point %s with the RBF %s and the polynomial order %s. This '
            'may be due to a stencil with duplicate or collinear points. '
            'The stencil contains the following points:\n%s' %
            (x, basis, order, s))
Example #27
0
def weight_matrix(x,
                  p,
                  diffs,
                  coeffs=None,
                  basis=rbf.basis.phs3,
                  order=None,
                  eps=1.0,
                  n=None,
                  stencils=None):
    ''' 
  Returns a weight matrix which maps a functions values at `p` to an
  approximation of that functions derivative at `x`. This is a
  convenience function which first creates a stencil network and then
  computed the RBF-FD weights for each stencil.
  
  Parameters
  ----------
  x : (N, D) array
    Target points. 

  p : (M, D) array
    Source points. The stencils will be made up of these points.

  diffs : (D,) int array or (K, D) int array 
    Derivative orders for each spatial dimension. For example `[2, 0]`
    indicates that the weights should approximate the second
    derivative with respect to the first spatial dimension in
    two-dimensional space.  diffs can also be a (K, D) array, where
    each (D,) sub-array is a term in a differential operator. For
    example the two-dimensional Laplacian can be represented as
    `[[2, 0], [0, 2]]`.

  coeffs : (K,) float array or (K, N) float, optional 
    Coefficients for each term in the differential operator specified
    with `diffs`. Defaults to an array of ones. If `diffs` was
    specified as a (D,) array then `coeffs` should be a length 1
    array. If the coefficients for the differential operator vary with
    `x` then `coeffs` can be specified as a (K, N) array.

  basis : rbf.basis.RBF, optional
    Type of RBF. Select from those available in `rbf.basis` or create 
    your own.

  order : int, optional
    Order of the added polynomial. This defaults to the highest
    derivative order. For example, if `diffs` is `[[2, 0], [0, 1]]`, then
    `order` is set to 2.

  eps : float or (M,) array, optional
    shape parameter for each RBF, which have centers `p`. This only 
    makes a difference when using RBFs that are not scale invariant.  
    All the predefined RBFs except for the odd order polyharmonic 
    splines are not scale invariant.

  n : int, optional
    Stencil size.
    
  stencils : (N, n) int array, optional
    The stencils for each node in `x`. This consists of indices of
    nodes in `p` that make up each stencil. If this is given then the
    value for `n` will be ignored. If this is not given then the
    stencils will be created based on nearest neighbors.
    

  Returns
  -------
  (N, M) csc sparse matrix          
      
  Examples
  --------
  Create a second order differentiation matrix in one-dimensional 
  space

  >>> x = np.arange(4.0)[:, None]
  >>> W = weight_matrix(x, x, (2,))
  >>> W.toarray()
  array([[ 1., -2.,  1.,  0.],
         [ 1., -2.,  1.,  0.],
         [ 0.,  1., -2.,  1.],
         [ 0.,  1., -2.,  1.]])
                         
  '''
    x = np.asarray(x, dtype=float)
    assert_shape(x, (None, None), 'x')

    p = np.asarray(p, dtype=float)
    assert_shape(p, (None, x.shape[1]), 'p')

    diffs = np.asarray(diffs, dtype=int)
    diffs = _reshape_diffs(diffs)

    if np.isscalar(eps):
        eps = np.full(p.shape[0], eps, dtype=float)
    else:
        eps = np.asarray(eps, dtype=float)
        assert_shape(eps, (p.shape[0], ), 'eps')

    # make `coeffs` a (K, N) array
    if coeffs is None:
        coeffs = np.ones((diffs.shape[0], x.shape[0]), dtype=float)
    else:
        coeffs = np.asarray(coeffs, dtype=float)
        if coeffs.ndim == 1:
            coeffs = np.repeat(coeffs[:, None], x.shape[0], axis=1)

        assert_shape(coeffs, (diffs.shape[0], x.shape[0]), 'coeffs')

    if stencils is None:
        if n is None:
            # if stencil size is not given then use the default stencil
            # size. Make sure that this is no larger than `p`
            n = _default_stencil_size(diffs)
            n = min(n, p.shape[0])

        stencils = rbf.stencil.stencil_network(x, p, n)
    else:
        stencils = np.asarray(stencils, dtype=int)
        assert_shape(stencils, (x.shape[0], None), 'stencils')

    logger.debug(
        'building a (%s, %s) RBF-FD weight matrix with %s nonzeros...' %
        (x.shape[0], p.shape[0], stencils.size))

    # values that will be put into the sparse matrix
    data = np.zeros(stencils.shape, dtype=float)
    for i, si in enumerate(stencils):
        # intermittently log the progress
        if i % max(stencils.shape[0] // 10, 1) == 0:
            logger.debug('  %d%% complete' % (100 * i / stencils.shape[0]))

        data[i, :] = weights(x[i],
                             p[si],
                             diffs,
                             coeffs=coeffs[:, i],
                             eps=eps[si],
                             basis=basis,
                             order=order)

    rows = np.repeat(range(data.shape[0]), data.shape[1])
    cols = stencils.ravel()
    data = data.ravel()
    shape = x.shape[0], p.shape[0]
    L = sp.csc_matrix((data, (rows, cols)), shape)
    logger.debug('  done')
    return L
Example #28
0
  def __call__(self, x, c, eps=1.0, diff=None):
    ''' 
    Numerically evaluates the RBF or its derivatives.
    
    Parameters                                       
    ----------                                         
    x : (N, D) float array 
      Evaluation points
                                                                       
    c : (M, D) float array 
      RBF centers 
        
    eps : float or (M,) float array, optional
      Shape parameters for each RBF. Defaults to 1.0
                                                                           
    diff : (D,) int array, optional
      Specifies the derivative order for each spatial dimension. For example,
      if there are three spatial dimensions then providing (2, 0, 1) would
      cause this function to return the RBF after differentiating it twice
      along the first dimension and once along the third dimension.

    Returns
    -------
    (N, M) float array
      The RBFs with centers `c` evaluated at `x`

    Notes
    -----
    * The default method for converting the symbolic RBF to a numeric function
      limits the number of spatial dimensions `D` to 15. There is no such
      limitation when the conversion method is set to "lambdify". Set the
      conversion method using the function `set_symbolic_to_numeric_method`.

    * The derivative order can be arbitrarily high, but some RBFs, such as
      Wendland and Matern, become numerically unstable when the derivative
      order exceeds 2.

    '''
    x = np.asarray(x, dtype=float)
    assert_shape(x, (None, None), 'x')

    c = np.asarray(c, dtype=float)
    assert_shape(c, (None, x.shape[1]), 'c')

    # If `eps` is not a scalar, then it should be an array with the same length
    # as `c`.
    if not np.isscalar(eps):
      eps = np.asarray(eps, dtype=float)
      assert_shape(eps, (c.shape[0],), 'eps')

    # if `diff` is not given then take no derivatives
    if diff is None:
      diff = (0,)*x.shape[1]

    else:
      # make sure diff is immutable
      diff = tuple(diff)
      assert_shape(diff, (x.shape[1],), 'diff')

    # add numerical function to cache if not already
    if diff not in self._cache:
      self._add_diff_to_cache(diff)

    # expand to allow for broadcasting
    x = x.T[:, :, None] 
    c = c.T[:, None, :]
    args = (tuple(x) + tuple(c) + (eps,))
    # evaluate the cached function for the given `x`, `c`, and `eps`
    out = self._cache[diff](*args)
    return out
Example #29
0
    def __call__(self, x, diff=None, chunk_size=100):
        '''
    Evaluates the interpolant at `x`

    Parameters
    ----------
    x : (N, D) float array
      Target points

    diff : (D,) int array, optional
      Derivative order for each spatial dimension

    chunk_size : int, optional
      Break `x` into chunks with this size and evaluate the interpolant for
      each chunk

    Returns
    -------
    (N,) float array

    '''
        x = np.asarray(x, dtype=float)
        assert_shape(x, (None, self.y.shape[1]), 'x')
        nx = x.shape[0]

        if chunk_size is not None:
            out = np.zeros(nx, dtype=float)
            for start in range(0, nx, chunk_size):
                stop = start + chunk_size
                out[start:stop] = self(x[start:stop],
                                       diff=diff,
                                       chunk_size=None)

            return out

        # get the indices of the k-nearest observations for each interpolation
        # point
        _, nbr = self.tree.query(x, self.k)
        # multiple interpolation points may have the same neighborhood. Make the
        # neighborhoods unique so that we only compute the interpolation
        # coefficients once for each neighborhood
        nbr, inv = np.unique(np.sort(nbr, axis=1), return_inverse=True, axis=0)
        nnbr = nbr.shape[0]
        # Get the observation data for each neighborhood
        y, d, sigma = self.y[nbr], self.d[nbr], self.sigma[nbr]
        # shift the centers of each neighborhood to zero for numerical stability
        centers = y.mean(axis=1)
        y = y - centers[:, None]
        # build the left-hand-side interpolation matrix consisting of the RBF
        # and monomials evaluated at each neighborhood
        Kyy = self.phi(y, y, eps=self.eps)
        Kyy[:, range(self.k), range(self.k)] += sigma**2
        Py = mvmonos(y, self.order)
        PyT = np.transpose(Py, (0, 2, 1))
        nmonos = Py.shape[2]
        Z = np.zeros((nnbr, nmonos, nmonos), dtype=float)
        LHS = np.block([[Kyy, Py], [PyT, Z]])
        # build the right-hand-side data vector consisting of the observations for
        # each neighborhood and extra zeros
        z = np.zeros((nnbr, nmonos), dtype=float)
        rhs = np.hstack((d, z))
        # solve for the RBF and polynomial coefficients for each neighborhood
        coeff = np.linalg.solve(LHS, rhs)
        # expand the arrays from having one entry per neighborhood to one entry per
        # interpolation point
        coeff = coeff[inv]
        y = y[inv]
        centers = centers[inv]
        # evaluate at the interpolation points
        x = x - centers
        phi_coeff = coeff[:, :self.k]
        poly_coeff = coeff[:, self.k:]
        Kxy = self.phi(x[:, None], y, eps=self.eps, diff=diff)[:, 0]
        Px = mvmonos(x, self.order, diff=diff)
        out = (Kxy * phi_coeff).sum(axis=1) + (Px * poly_coeff).sum(axis=1)
        return out
Example #30
0
def min_energy_nodes(N,
                     vert,
                     smp,
                     rho=None,
                     pinned_nodes=None,
                     itr=100,
                     m=None,
                     delta=0.05,
                     snap_delta=0.5,
                     boundary_groups=None,
                     boundary_groups_with_ghosts=None,
                     include_vertices=False,
                     bound_force=False):
    '''
  Generates nodes within a 1, 2, or 3 dimensional domain using a
  minimum energy algorithm.

  The algorithm is as follows: A quasi-random set of nodes is first
  generated within the domain from a Halton sequence. The nodes
  positions are then iteratively adjusted. For each iteration, the
  nearest neighbors to each node are found. A repulsion force is
  calculated for each node using the distance to its nearest neighbors
  and their charges (which are inversely proportional to the node
  density). Each node then moves in the direction of the net force
  acting on it. If a node is repelled into boundary, it will bounce
  back into the domain. When the iteration are complete, nodes that
  are sufficiently close to the boundary are snapped to the boundary.
  This function returns the nodes, the normal vectors to the boundary
  nodes, and an index set indicating which nodes belong to which
  group.

  Parameters
  ----------
  N : int
    Number of nodes

  vert : (P, D) array
    Vertices making up the boundary

  smp : (Q, D) array
    Describes how the vertices are connected to form the boundary

  rho : function, optional
    Node density function. Takes a (?, D) array of coordinates in D
    dimensional space and returns an (?,) array of densities which
    have been normalized so that the maximum density in the domain is
    1.0. This function will still work if the maximum value is
    normalized to something less than 1.0; however it will be less
    efficient.

  pinned_nodes : (F, D) array, optional
    Nodes which do not move and only provide a repulsion force. These
    nodes are included in the set of nodes returned by this function
    and they are in the group named "pinned".

  itr : int, optional
    Number of repulsion iterations. If this number is small then the
    nodes will not reach a minimum energy equilibrium.

  m : int, optional
    Number of neighboring nodes to use when calculating the repulsion
    force. This defaults to 7 for 2D nodes and 13 for 3D nodes.
    Deviating from these default values may yield a node distribution
    that is not consistent with the node density function `rho`. 

  delta : float, optional
    Scaling factor for the node step size in each iteration. The
    step size is equal to `delta` times the distance to the nearest
    neighbor.

  snap_delta : float, optional
    Controls the maximum snapping distance. The maximum snapping
    distance for each node is `snap_delta` times the distance to the
    nearest neighbor. This defaults to 0.5.

  boundary_groups: dict, optional 
    Dictionary defining the boundary groups. The keys are the names of
    the groups and the values are lists of simplex indices making up
    each group. This function will return a dictionary identifying
    which nodes belong to each boundary group. By default, there is a
    group for each simplex making up the boundary and another group
    named 'all' for the entire boundary. Specifically, The default
    value is `{'all':range(len(smp)), '0':[0], '1':[1], ...}`.

  boundary_groups_with_ghosts: list of strs, optional
    List of boundary groups that will be given ghost nodes. By
    default, no boundary groups are given ghost nodes. The groups
    specified here must exist in `boundary_groups`.

  bound_force : bool, optional
    If `True`, then nodes cannot repel other nodes through the domain
    boundary. Set this to `True` if the domain has edges that nearly
    touch eachother. Setting this to `True` may significantly increase
    computation time.

  include_vertices : bool, optional
    If `True`, then the vertices will be included in the output nodes.
    Each vertex will be assigned to the boundary group that its
    adjoining simplices are part of. If the simplices are in multiple
    groups, then the vertex will be assigned to the group containing
    the simplex that comes first in `smp`.

  Returns
  -------
  (N, D) float array
    Nodes positions

  dict 
    The indices of nodes belonging to each group. There will always be
    a group called 'interior' containing the nodes that are not on the
    boundary. By default there is a group containing all the boundary
    nodes called 'boundary:all', and there are groups containing the
    boundary nodes for each simplex called 'boundary:0', 'boundary:1',
    ..., 'boundary:Q'. If `boundary_groups` was specified, then those
    groups will be included in this dictionary and their names will be
    given a 'boundary:' prefix. If `boundary_groups_with_ghosts` was
    specified then those groups of ghost nodes will be included in
    this dictionary and their names will be given a 'ghosts:' prefix.
    
  (N, D) float array
    Outward normal vectors for each node. If a node is not on the
    boundary then its corresponding row will contain NaNs.

  Notes
  -----
  It is assumed that `vert` and `smp` define a closed domain. If
  this is not the case, then it is likely that an error message will
  be raised which says "ValueError: No intersection found for
  segment ...".

  Examples
  --------
  make 9 nodes within the unit square   

  >>> vert = np.array([[0, 0], [1, 0], [1, 1], [0, 1]])  
  >>> smp = np.array([[0, 1], [1, 2], [2, 3], [3, 0]])
  >>> out = min_energy_nodes(9, vert, smp)  

  view the nodes

  >>> out[0]  
  array([[ 0.50325675,  0.        ],
         [ 0.00605261,  1.        ],
         [ 1.        ,  0.51585247],
         [ 0.        ,  0.00956821],
         [ 1.        ,  0.99597894],
         [ 0.        ,  0.5026365 ],
         [ 1.        ,  0.00951112],
         [ 0.48867638,  1.        ],
         [ 0.54063894,  0.47960892]])

  view the indices of nodes making each group

  >>> out[1] 
  {'boundary:0': array([0]),
   'boundary:1': array([6, 4, 2]),
   'boundary:2': array([7, 1]),
   'boundary:3': array([5, 3]),
   'boundary:all': array([7, 6, 5, 4, 3, 2, 1, 0]),
   'interior': array([8])}

  view the outward normal vectors for each node, note that the normal
  vector for the interior node is `nan`

  >>> out[2] 
  array([[  0.,  -1.],
         [  0.,   1.],
         [  1.,  -0.],
         [ -1.,  -0.],
         [  1.,  -0.],
         [ -1.,  -0.],
         [  1.,  -0.],
         [  0.,   1.],
         [ nan,  nan]])
    
  '''
    logger.debug('starting minimum energy node generation')
    vert = np.asarray(vert, dtype=float)
    assert_shape(vert, (None, None), 'vert')

    smp = np.asarray(smp, dtype=int)
    assert_shape(smp, (None, vert.shape[1]), 'smp')

    if boundary_groups is None:
        boundary_groups = {'all': range(smp.shape[0])}
        for i in range(smp.shape[0]):
            boundary_groups[str(i)] = [i]

    if pinned_nodes is None:
        pinned_nodes = np.zeros((0, vert.shape[1]), dtype=float)
    else:
        pinned_nodes = np.array(pinned_nodes, dtype=float)

    assert_shape(pinned_nodes, (None, vert.shape[1]), 'pinned_nodes')

    logger.debug('finding node positions with rejection sampling')
    nodes = _rejection_sampling_nodes(N, vert, smp, rho=rho)

    # `pinned_nodes` consist of specific nodes that we want included in
    # the output nodes. If `include_vertices` is True then add the
    # vertices to the pinned nodes, labeling the combination as
    # `pinned_nodes_`
    if include_vertices:
        pinned_nodes_ = np.vstack((pinned_nodes, vert))
    else:
        pinned_nodes_ = pinned_nodes

    # use a minimum energy algorithm to spread out the nodes
    for i in range(itr):
        logger.debug('starting node repulsion iteration %s of %s' %
                     (i + 1, itr))
        nodes = _disperse_within_boundary(nodes,
                                          vert,
                                          smp,
                                          rho=rho,
                                          pinned_nodes=pinned_nodes_,
                                          m=m,
                                          delta=delta,
                                          bound_force=bound_force)

    nodes, smpid = _snap_to_boundary(nodes, vert, smp, delta=snap_delta)
    normals = _make_normal_vectors(smpid, vert, smp)
    groups = _make_group_indices(smpid, boundary_groups)

    if include_vertices:
        nodes, groups, normals = _append_vertices(nodes, groups, normals, vert,
                                                  smp, boundary_groups)

    if pinned_nodes.size != 0:
        # append the pinned nodes to the output
        groups['pinned'] = np.arange(nodes.shape[0],
                                     nodes.shape[0] + pinned_nodes.shape[0])
        normals = np.vstack((normals, np.full_like(pinned_nodes, np.nan)))
        nodes = np.vstack((nodes, pinned_nodes))

    if boundary_groups_with_ghosts is not None:
        nodes, groups, normals = _append_ghost_nodes(
            nodes, groups, normals, boundary_groups_with_ghosts)

    # sort `nodes` so that spatially adjacent nodes are close together
    # in memory. Update `indices` so that it is still pointing to the
    # same nodes
    nodes, groups, normals = _sort_nodes(nodes, groups, normals)

    # verify that the nodes are not too close to eachother
    _test_node_spacing(nodes, rho)

    logger.debug('finished generating %s nodes' % nodes.shape[0])
    return nodes, groups, normals