|
| 1 | +import itertools |
| 2 | + |
| 3 | +import numpy as np |
| 4 | +from scipy import sparse |
| 5 | + |
| 6 | +from findiff.coefs import coefficients_non_uni, coefficients |
| 7 | +from findiff.grids import GridAxis, EquidistantAxis, NonEquidistantAxis |
| 8 | +from findiff.utils import long_indices_as_ndarray, to_long_index |
| 9 | + |
| 10 | + |
| 11 | +def build_differentiator(order: int, axis: GridAxis, acc): |
| 12 | + if isinstance(axis, EquidistantAxis): |
| 13 | + if not axis.periodic: |
| 14 | + return _FinDiffUniform(axis.dim, order, axis.spacing, acc) |
| 15 | + else: |
| 16 | + return _FinDiffUniformPeriodic(axis.dim, order, axis.spacing, acc) |
| 17 | + elif isinstance(axis, NonEquidistantAxis): |
| 18 | + if not axis.periodic: |
| 19 | + return _FinDiffNonUniform(axis.dim, order, axis.coords, acc) |
| 20 | + else: |
| 21 | + raise NotImplementedError("Periodic nonuniform axes not yet implemented") |
| 22 | + else: |
| 23 | + raise TypeError("Unknown axis type.") |
| 24 | + |
| 25 | + |
| 26 | +class _FinDiffBase: |
| 27 | + |
| 28 | + def __init__(self, axis, order): |
| 29 | + self.axis = axis |
| 30 | + self.order = order |
| 31 | + |
| 32 | + def validate_f(self, f): |
| 33 | + try: |
| 34 | + f.shape[self.axis] |
| 35 | + except AttributeError as err: |
| 36 | + raise ValueError( |
| 37 | + "Diff objects can only be applied to arrays or evaluated(!) functions returning arrays" |
| 38 | + ) from err |
| 39 | + |
| 40 | + def apply_to_array(self, yd, y, weights, off_slices, ref_slice, dim): |
| 41 | + """Applies the finite differences only to slices along a given axis""" |
| 42 | + |
| 43 | + ndims = len(y.shape) |
| 44 | + |
| 45 | + all = slice(None, None, 1) |
| 46 | + |
| 47 | + ref_multi_slice = [all] * ndims |
| 48 | + ref_multi_slice[dim] = ref_slice |
| 49 | + |
| 50 | + for w, s in zip(weights, off_slices): |
| 51 | + off_multi_slice = [all] * ndims |
| 52 | + off_multi_slice[dim] = s |
| 53 | + if abs(1 - w) < 1.0e-14: |
| 54 | + yd[tuple(ref_multi_slice)] += y[tuple(off_multi_slice)] |
| 55 | + else: |
| 56 | + yd[tuple(ref_multi_slice)] += w * y[tuple(off_multi_slice)] |
| 57 | + |
| 58 | + def shift_slice(self, sl, off, max_index): |
| 59 | + |
| 60 | + if sl.start + off < 0 or sl.stop + off > max_index: |
| 61 | + raise IndexError("Shift slice out of bounds") |
| 62 | + |
| 63 | + return slice(sl.start + off, sl.stop + off, sl.step) |
| 64 | + |
| 65 | + |
| 66 | +class _FinDiffUniform(_FinDiffBase): |
| 67 | + |
| 68 | + def __init__(self, axis, order, spacing, acc): |
| 69 | + super().__init__(axis, order) |
| 70 | + self.spacing = spacing |
| 71 | + self.acc = acc |
| 72 | + coef_schemes = coefficients(self.order, acc) |
| 73 | + self.forward = coef_schemes["forward"] |
| 74 | + self.backward = coef_schemes["backward"] |
| 75 | + self.center = coef_schemes["center"] |
| 76 | + |
| 77 | + def __call__(self, f): |
| 78 | + self.validate_f(f) |
| 79 | + npts = f.shape[self.axis] |
| 80 | + weights = self.center["coefficients"] |
| 81 | + offsets = self.center["offsets"] |
| 82 | + |
| 83 | + num_bndry_points = len(weights) // 2 |
| 84 | + ref_slice = slice(num_bndry_points, npts - num_bndry_points, 1) |
| 85 | + off_slices = [ |
| 86 | + self.shift_slice(ref_slice, offsets[k], npts) for k in range(len(offsets)) |
| 87 | + ] |
| 88 | + |
| 89 | + fd = np.zeros_like(f) |
| 90 | + |
| 91 | + self.apply_to_array(fd, f, weights, off_slices, ref_slice, self.axis) |
| 92 | + |
| 93 | + weights = self.forward["coefficients"] |
| 94 | + offsets = self.forward["offsets"] |
| 95 | + |
| 96 | + ref_slice = slice(0, num_bndry_points, 1) |
| 97 | + off_slices = [ |
| 98 | + self.shift_slice(ref_slice, offsets[k], npts) for k in range(len(offsets)) |
| 99 | + ] |
| 100 | + |
| 101 | + self.apply_to_array(fd, f, weights, off_slices, ref_slice, self.axis) |
| 102 | + |
| 103 | + weights = self.backward["coefficients"] |
| 104 | + offsets = self.backward["offsets"] |
| 105 | + |
| 106 | + ref_slice = slice(npts - num_bndry_points, npts, 1) |
| 107 | + off_slices = [ |
| 108 | + self.shift_slice(ref_slice, offsets[k], npts) for k in range(len(offsets)) |
| 109 | + ] |
| 110 | + |
| 111 | + self.apply_to_array(fd, f, weights, off_slices, ref_slice, self.axis) |
| 112 | + |
| 113 | + h_inv = 1.0 / self.spacing**self.order |
| 114 | + return fd * h_inv |
| 115 | + |
| 116 | + def matrix(self, shape): |
| 117 | + |
| 118 | + h = self.spacing |
| 119 | + |
| 120 | + ndims = len(shape) |
| 121 | + siz = np.prod(shape) |
| 122 | + long_indices_nd = long_indices_as_ndarray(shape) |
| 123 | + |
| 124 | + axis, order = self.axis, self.order |
| 125 | + mat = sparse.lil_matrix((siz, siz)) |
| 126 | + |
| 127 | + for scheme in ["center", "forward", "backward"]: |
| 128 | + |
| 129 | + offsets_1d = getattr(self, scheme)["offsets"] |
| 130 | + coeffs = getattr(self, scheme)["coefficients"] |
| 131 | + |
| 132 | + # translate offsets of given scheme to long format |
| 133 | + offsets_long = [] |
| 134 | + for o_1d in offsets_1d: |
| 135 | + o_nd = np.zeros(ndims) |
| 136 | + o_nd[axis] = o_1d |
| 137 | + o_long = to_long_index(o_nd, shape) |
| 138 | + offsets_long.append(o_long) |
| 139 | + |
| 140 | + # determine points where to evaluate current scheme in long format |
| 141 | + nside = len(self.center["coefficients"]) // 2 |
| 142 | + if scheme == "center": |
| 143 | + multi_slice = [slice(None, None)] * ndims |
| 144 | + multi_slice[axis] = slice(nside, -nside) |
| 145 | + Is = long_indices_nd[tuple(multi_slice)].reshape(-1) |
| 146 | + elif scheme == "forward": |
| 147 | + multi_slice = [slice(None, None)] * ndims |
| 148 | + multi_slice[axis] = slice(0, nside) |
| 149 | + Is = long_indices_nd[tuple(multi_slice)].reshape(-1) |
| 150 | + else: |
| 151 | + multi_slice = [slice(None, None)] * ndims |
| 152 | + multi_slice[axis] = slice(-nside, None) |
| 153 | + Is = long_indices_nd[tuple(multi_slice)].reshape(-1) |
| 154 | + |
| 155 | + for o, c in zip(offsets_long, coeffs): |
| 156 | + v = c / h**order |
| 157 | + mat[Is, Is + o] = v |
| 158 | + |
| 159 | + return mat |
| 160 | + |
| 161 | + |
| 162 | +class _FinDiffUniformPeriodic(_FinDiffBase): |
| 163 | + |
| 164 | + def __init__(self, axis, order, spacing, acc): |
| 165 | + super().__init__(axis, order) |
| 166 | + self.spacing = spacing |
| 167 | + self.acc = acc |
| 168 | + self.coefs = coefficients(self.order, acc)["center"] |
| 169 | + |
| 170 | + def __call__(self, f): |
| 171 | + self.validate_f(f) |
| 172 | + fd = np.zeros_like(f) |
| 173 | + for off, coef in zip(self.coefs["offsets"], self.coefs["coefficients"]): |
| 174 | + fd += coef * np.roll(f, -off, axis=self.axis) |
| 175 | + h_inv = 1.0 / self.spacing**self.order |
| 176 | + return fd * h_inv |
| 177 | + |
| 178 | + def matrix(self, shape): |
| 179 | + h = self.spacing |
| 180 | + |
| 181 | + ndims = len(shape) |
| 182 | + siz = np.prod(shape) |
| 183 | + long_indices_nd = long_indices_as_ndarray(shape) |
| 184 | + |
| 185 | + axis, order = self.axis, self.order |
| 186 | + mat = sparse.lil_matrix((siz, siz)) |
| 187 | + |
| 188 | + offsets = self.coefs["offsets"] |
| 189 | + coefs = self.coefs["coefficients"] |
| 190 | + |
| 191 | + multi_slice = [slice(None, None)] * ndims |
| 192 | + Is = long_indices_nd[tuple(multi_slice)].reshape(-1) |
| 193 | + |
| 194 | + idxs_short = [np.arange(n) for n in shape] |
| 195 | + |
| 196 | + for o, c in zip(offsets, coefs): |
| 197 | + v = c / h**order |
| 198 | + |
| 199 | + idxs_short[self.axis] = np.roll(np.arange(shape[self.axis]), -o) |
| 200 | + grid = np.meshgrid(*idxs_short, indexing="ij") |
| 201 | + index_tuples = np.stack(grid, axis=-1).reshape(-1, ndims) |
| 202 | + |
| 203 | + Is_off = np.ravel_multi_index(index_tuples.T, shape) |
| 204 | + |
| 205 | + mat[Is, Is_off] = v |
| 206 | + |
| 207 | + return mat |
| 208 | + |
| 209 | + |
| 210 | +class _FinDiffNonUniform(_FinDiffBase): |
| 211 | + def __init__(self, axis, order, coords, acc): |
| 212 | + super().__init__(axis, order) |
| 213 | + self.coords = coords |
| 214 | + self.acc = acc |
| 215 | + self.coef_list = [] |
| 216 | + for i in range(len(self.coords)): |
| 217 | + self.coef_list.append(coefficients_non_uni(order, self.acc, self.coords, i)) |
| 218 | + |
| 219 | + def __call__(self, y): |
| 220 | + """The core function to take a partial derivative on a non-uniform grid""" |
| 221 | + |
| 222 | + order, dim = self.order, self.axis |
| 223 | + yd = np.zeros_like(y) |
| 224 | + |
| 225 | + ndims = len(y.shape) |
| 226 | + multi_slice = [slice(None, None)] * ndims |
| 227 | + ref_multi_slice = [slice(None, None)] * ndims |
| 228 | + |
| 229 | + for i, x in enumerate(self.coords): |
| 230 | + |
| 231 | + coefs = self.coef_list[i] |
| 232 | + weights = coefs["coefficients"] |
| 233 | + offsets = coefs["offsets"] |
| 234 | + ref_multi_slice[dim] = i |
| 235 | + |
| 236 | + for off, w in zip(offsets, weights): |
| 237 | + multi_slice[dim] = i + off |
| 238 | + yd[tuple(ref_multi_slice)] += w * y[tuple(multi_slice)] |
| 239 | + |
| 240 | + return yd |
| 241 | + |
| 242 | + def matrix(self, shape): |
| 243 | + |
| 244 | + coords = self.coords |
| 245 | + |
| 246 | + siz = np.prod(shape) |
| 247 | + long_inds = np.arange(siz).reshape(shape) |
| 248 | + short_inds = [np.arange(shape[k]) for k in range(len(shape))] |
| 249 | + short_inds = list(itertools.product(*short_inds)) |
| 250 | + |
| 251 | + coef_dicts = [] |
| 252 | + for i in range(len(coords)): |
| 253 | + coef_dicts.append(coefficients_non_uni(self.order, self.acc, coords, i)) |
| 254 | + |
| 255 | + mat = sparse.lil_matrix((siz, siz)) |
| 256 | + |
| 257 | + for base_ind_long, base_ind_short in enumerate(short_inds): |
| 258 | + cd = coef_dicts[base_ind_short[self.axis]] |
| 259 | + cs, os = cd["coefficients"], cd["offsets"] |
| 260 | + for c, o in zip(cs, os): |
| 261 | + off_short = np.zeros(len(shape), dtype=int) |
| 262 | + off_short[self.axis] = int(o) |
| 263 | + off_ind_short = np.array(base_ind_short, dtype=int) + off_short |
| 264 | + off_long = long_inds[tuple(off_ind_short)] |
| 265 | + |
| 266 | + mat[base_ind_long, off_long] += c |
| 267 | + |
| 268 | + return mat |
0 commit comments