Skip to content

Commit 3b89c08

Browse files
committed
Refactoring
1 parent b7266f2 commit 3b89c08

File tree

10 files changed

+527
-457
lines changed

10 files changed

+527
-457
lines changed

findiff/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
__version__ = "0.12.0"
2323

2424

25-
from .operators import Diff, Identity
25+
from .interface import Diff
26+
from .operators import Identity
2627
from .pde import PDE, BoundaryConditions
2728
from .compatible import Coef, Coefficient, FinDiff, Id
2829
from .coefs import coefficients

findiff/compatible.py

+67-10
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,74 @@
1-
"""Provides an interface to obsolete classes for backward compatibility."""
1+
"""This module provides an interface to obsolete classes for backward compatibility."""
22

33
from findiff import Diff
4-
from findiff.legacy.operators import _FinDiff
54
from findiff.operators import FieldOperator, Identity
65

76

8-
# @deprecated(reason="Use findiff.Diff instead.")
97
def FinDiff(*args, **kwargs):
8+
r"""A representation of a general linear differential operator expressed in finite differences.
109
10+
FinDiff objects can be added with other FinDiff objects. They can be multiplied by
11+
objects of type Coefficient.
12+
13+
FinDiff is callable, i.e. to apply the derivative, just call the object on the array to
14+
differentiate.
15+
16+
:param args: variable number of tuples. Defines what derivative to take.
17+
If only one tuple is given, you can leave away the tuple parentheses.
18+
19+
Each tuple has the form
20+
21+
`(axis, spacing, count)` for uniform grids
22+
23+
`(axis, count)` for non-uniform grids.
24+
25+
`axis` is the dimension along which to take derivative.
26+
27+
`spacing` is the grid spacing of the uniform grid along that axis.
28+
29+
`count` is the order of the derivative, which is optional an defaults to 1.
30+
31+
32+
:param kwargs: variable number of keyword arguments
33+
34+
Allowed keywords:
35+
36+
`acc`: even integer
37+
The desired accuracy order. Default is acc=2.
38+
39+
This class is actually deprecated and will be replaced by the Diff class in the future.
40+
41+
**Example**:
42+
43+
44+
For this example, we want to operate on some 3D array f:
45+
46+
>>> import numpy as np
47+
>>> x, y, z = [np.linspace(-1, 1, 100) for _ in range(3)]
48+
>>> X, Y, Z = np.meshgrid(x, y, z, indexing='ij')
49+
>>> f = X**2 + Y**2 + Z**2
50+
51+
To create :math:`\\frac{\\partial f}{\\partial x}` on a uniform grid with spacing dx, dy
52+
along the 0th axis or 1st axis, respectively, instantiate a FinDiff object and call it:
53+
54+
>>> d_dx = FinDiff(0, dx)
55+
>>> d_dy = FinDiff(1, dx)
56+
>>> result = d_dx(f)
57+
58+
For :math:`\\frac{\\partial^2 f}{\\partial x^2}` or :math:`\\frac{\\partial^2 f}{\\partial y^2}`:
59+
60+
>>> d2_dx2 = FinDiff(0, dx, 2)
61+
>>> d2_dy2 = FinDiff(1, dy, 2)
62+
>>> result_2 = d2_dx2(f)
63+
>>> result_3 = d2_dy2(f)
64+
65+
For :math:`\\frac{\\partial^4 f}{\partial x \\partial^2 y \\partial z}`, do:
66+
67+
>>> op = FinDiff((0, dx), (1, dy, 2), (2, dz))
68+
>>> result_4 = op(f)
69+
70+
71+
"""
1172
if len(args) > 3:
1273
raise ValueError("FinDiff accepts not more than 3 positional arguments.")
1374

@@ -31,15 +92,11 @@ def diff_from_tuple(tpl):
3192
return diff_from_tuple(args)
3293

3394

34-
FinDiff.__doc__ = _FinDiff.__doc__
35-
36-
37-
"""
38-
Define aliasses for backward compatibility:
39-
"""
95+
###
96+
### Define aliasses for backward compatibility:
97+
###
4098

4199

42-
# @deprecated(reason="No need to wrap array in Coefficient object any more.")
43100
class Coefficient(FieldOperator):
44101
pass
45102

findiff/findiff.py

+268
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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

Comments
 (0)