Skip to content

Commit 6e7f8fa

Browse files
committed
Replace asarray_of_arrays() with XyzComponents
1 parent 08750df commit 6e7f8fa

File tree

3 files changed

+100
-26
lines changed

3 files changed

+100
-26
lines changed

sfs/mono/source.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def point(omega, x0, n0, grid, c=None):
5353
"""
5454
k = util.wavenumber(omega, c)
5555
x0 = util.asarray_1d(x0)
56-
grid = util.asarray_of_arrays(grid)
56+
grid = util.XyzComponents(grid)
5757

5858
r = np.linalg.norm(grid - x0)
5959
return 1 / (4*np.pi) * np.exp(-1j * k * r) / r
@@ -65,7 +65,7 @@ def point_velocity(omega, x0, n0, grid, c=None):
6565
"""
6666
k = util.wavenumber(omega, c)
6767
x0 = util.asarray_1d(x0)
68-
grid = util.asarray_of_arrays(grid)
68+
grid = util.XyzComponents(grid)
6969
offset = grid - x0
7070
r = np.linalg.norm(offset)
7171
v = point(omega, x0, n0, grid, c=c)
@@ -108,7 +108,7 @@ def point_modal(omega, x0, n0, grid, L, N=None, deltan=0, c=None):
108108
"""
109109
k = util.wavenumber(omega, c)
110110
x0 = util.asarray_1d(x0)
111-
x, y, z = util.asarray_of_arrays(grid)
111+
x, y, z = util.XyzComponents(grid)
112112

113113
if N is None:
114114
# determine maximum modal order per dimension
@@ -178,7 +178,7 @@ def point_modal_velocity(omega, x0, n0, grid, L, N=None, deltan=0, c=None):
178178
"""
179179
k = util.wavenumber(omega, c)
180180
x0 = util.asarray_1d(x0)
181-
x, y, z = util.asarray_of_arrays(grid)
181+
x, y, z = util.XyzComponents(grid)
182182

183183
if N is None:
184184
# determine maximum modal order per dimension
@@ -251,7 +251,7 @@ def line(omega, x0, n0, grid, c=None):
251251
k = util.wavenumber(omega, c)
252252
x0 = util.asarray_1d(x0)
253253
x0 = x0[:2] # ignore z-component
254-
grid = util.asarray_of_arrays(grid)
254+
grid = util.XyzComponents(grid)
255255

256256
r = np.linalg.norm(grid[:2] - x0)
257257
p = -1j/4 * special.hankel2(0, k * r)
@@ -265,7 +265,7 @@ def line_velocity(omega, x0, n0, grid, c=None):
265265
k = util.wavenumber(omega, c)
266266
x0 = util.asarray_1d(x0)
267267
x0 = x0[:2] # ignore z-component
268-
grid = util.asarray_of_arrays(grid)
268+
grid = util.XyzComponents(grid)
269269

270270
offset = grid[:2] - x0
271271
r = np.linalg.norm(offset)
@@ -296,7 +296,7 @@ def line_dipole(omega, x0, n0, grid, c=None):
296296
x0 = util.asarray_1d(x0)
297297
x0 = x0[:2] # ignore z-component
298298
n0 = n0[:2]
299-
grid = util.asarray_of_arrays(grid)
299+
grid = util.XyzComponents(grid)
300300
dx = grid[:2] - x0
301301
r = np.linalg.norm(dx)
302302
p = 1j*k/4 * special.hankel2(1, k * r) * np.inner(dx, n0) / r
@@ -325,7 +325,7 @@ def plane(omega, x0, n0, grid, c=None):
325325
k = util.wavenumber(omega, c)
326326
x0 = util.asarray_1d(x0)
327327
n0 = util.asarray_1d(n0)
328-
grid = util.asarray_of_arrays(grid)
328+
grid = util.XyzComponents(grid)
329329
return np.exp(-1j * k * np.inner(grid - x0, n0))
330330

331331

sfs/plot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def loudspeaker_2d(x0, n0, a0=0.5, size=0.08, show_numbers=False, grid=None,
145145

146146
def _visible_secondarysources_2d(x0, n0, grid):
147147
"""Determine secondary sources which lie within `grid`."""
148-
grid = util.asarray_of_arrays(grid)
148+
grid = util.XyzComponents(grid)
149149
x, y = grid[:2]
150150
idx = np.where((x0[:, 0] > x.min()) & (x0[:, 0] < x.max()) &
151151
(x0[:, 1] > y.min()) & (x0[:, 1] < x.max()))
@@ -242,7 +242,7 @@ def soundfield(p, grid, xnorm=None, cmap='coolwarm_clip', colorbar=True,
242242
243243
"""
244244
p = np.asarray(p)
245-
grid = util.asarray_of_arrays(grid)
245+
grid = util.XyzComponents(grid)
246246

247247
# normalize sound field wrt xnorm
248248
if xnorm is not None:

sfs/util.py

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -98,20 +98,6 @@ def asarray_of_rows(a, **kwargs):
9898
return result
9999

100100

101-
def asarray_of_arrays(a, **kwargs):
102-
"""Convert the input to an array consisting of arrays.
103-
104-
A one-dimensional :class:`numpy.ndarray` with `dtype=object` is
105-
returned, containing the elements of `a` as :class:`numpy.ndarray`
106-
(whose `dtype` and other options can be specified with `**kwargs`).
107-
108-
"""
109-
result = np.empty(len(a), dtype=object)
110-
for i, element in enumerate(a):
111-
result[i] = np.asarray(element, **kwargs)
112-
return result
113-
114-
115101
def strict_arange(start, stop, step=1, endpoint=False, dtype=None, **kwargs):
116102
"""Like :func:`numpy.arange`, but compensating numeric errors.
117103
@@ -202,7 +188,7 @@ def normalize(p, grid, xnorm):
202188

203189
def probe(p, grid, x):
204190
"""Determine the value at position `x` in the sound field `p`."""
205-
grid = asarray_of_arrays(grid)
191+
grid = XyzComponents(grid)
206192
x = asarray_1d(x)
207193
r = np.linalg.norm(grid - x)
208194
idx = np.unravel_index(r.argmin(), r.shape)
@@ -228,7 +214,7 @@ def displacement(v, omega):
228214
d(x, t) = \int_0^t v(x, t) dt
229215
230216
"""
231-
v = asarray_of_arrays(v)
217+
v = XyzComponents(v)
232218
return v / (1j * omega)
233219

234220

@@ -246,3 +232,91 @@ def db(x, power=False):
246232
"""
247233
with np.errstate(divide='ignore'):
248234
return 10 if power else 20 * np.log10(np.abs(x))
235+
236+
237+
class XyzComponents(np.ndarray):
238+
"""See __init__()."""
239+
240+
def __init__(self, arg, **kwargs):
241+
"""Triple (or pair) of arrays: x, y, and optionally z.
242+
243+
Instances of this class can be used to store coordinate grids
244+
(either regular grids like in :func:`sfs.util.xyz_grid` or
245+
arbitrary point clouds) or vector fields (e.g. particle
246+
velocity).
247+
248+
This class is a subclass of :class:`numpy.ndarray`. It is
249+
one-dimensional (like a plain :class:`list`) and has a length of
250+
3 (or 2, if no z-component is available). It uses
251+
`dtype=object` in order to be able to store other
252+
`numpy.ndarray`\s of arbitrary shapes but also scalars, if
253+
needed. Because it is a NumPy array subclass, it can be used in
254+
operations with scalars and "normal" NumPy arrays, as long as
255+
they have a compatible shape. Like any NumPy array, instances
256+
of this class are iterable and can be used, e.g., in for-loops
257+
and tuple unpacking. If slicing or broadcasting leads to an
258+
incompatible shape, a plain `numpy.ndarray` with `dtype=object`
259+
is returned.
260+
261+
Parameters
262+
----------
263+
arg : triple or pair of array_like
264+
The values to be used as X, Y and Z arrays. Z is optional.
265+
**kwargs
266+
All further arguments are forwarded to
267+
:func:`numpy.asarray`.
268+
269+
"""
270+
# This method does nothing, it's only here for the documentation!
271+
272+
def __new__(cls, arg, **kwargs):
273+
# object arrays cannot be created and populated in a single step:
274+
obj = np.ndarray.__new__(cls, len(arg), dtype=object)
275+
for i, component in enumerate(arg):
276+
obj[i] = np.asarray(component, **kwargs)
277+
return obj
278+
279+
def __array_finalize__(self, obj):
280+
if self.ndim == 0:
281+
pass # this is allowed, e.g. for np.inner()
282+
elif self.ndim > 1 or len(self) not in (2, 3):
283+
raise ValueError("XyzComponents can only have 2 or 3 components")
284+
285+
def __array_prepare__(self, obj, context=None):
286+
if obj.ndim == 1 and len(obj) in (2, 3):
287+
return obj.view(XyzComponents)
288+
return obj
289+
290+
def __array_wrap__(self, obj, context=None):
291+
if obj.ndim != 1 or len(obj) not in (2, 3):
292+
return obj.view(np.ndarray)
293+
return obj
294+
295+
def __getitem__(self, index):
296+
if isinstance(index, slice):
297+
start, stop, step = index.indices(len(self))
298+
if start == 0 and stop in (2, 3) and step == 1:
299+
return np.ndarray.__getitem__(self, index)
300+
# Slices other than xy and xyz are "downgraded" to ndarray
301+
return np.ndarray.__getitem__(self.view(np.ndarray), index)
302+
303+
def __repr__(self):
304+
return 'XyzComponents(\n' + ',\n'.join(
305+
' {0}={1}'.format(name, repr(data).replace('\n', '\n '))
306+
for name, data in zip('xyz', self)) + ')'
307+
308+
def make_property(index, doc):
309+
310+
def getter(self):
311+
return self[index]
312+
313+
def setter(self, value):
314+
self[index] = value
315+
316+
return property(getter, setter, doc=doc)
317+
318+
x = make_property(0, doc='x-component.')
319+
y = make_property(1, doc='y-component.')
320+
z = make_property(2, doc='z-component (optional).')
321+
322+
del make_property

0 commit comments

Comments
 (0)