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
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
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
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
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
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
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)
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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))
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
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
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
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