Skip to content

Commit 254f091

Browse files
committed
Some changes to array implementation and plotting
* add util.broadcast_zip() * add util.asarray_of_rows() * allow scalars in util.asarray_1d() * rename 'index' argument to 'show_numbers' in loudspeaker_2d()
1 parent ce06753 commit 254f091

File tree

3 files changed

+107
-67
lines changed

3 files changed

+107
-67
lines changed

sfs/array.py

Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,34 @@
99
plt.rcParams['axes.grid'] = True
1010
1111
"""
12-
12+
from __future__ import division # for Python 2.x
1313
import numpy as np
1414
from . import util
1515

1616

17-
def linear(N, dx, center=[0, 0, 0], n0=[1, 0, 0]):
17+
def linear(N, spacing, center=[0, 0, 0], n0=[1, 0, 0]):
1818
"""Linear secondary source distribution.
1919
20+
Parameters
21+
----------
22+
N : int
23+
Number of loudspeakers.
24+
spacing : float
25+
Distance (in metres) between loudspeakers.
26+
center : (3,) array_like, optional
27+
Coordinates of array center.
28+
n0 : (3,) array_like, optional
29+
Normal vector of array.
30+
31+
Returns
32+
-------
33+
positions : (N, 3) numpy.ndarray
34+
Positions of secondary sources
35+
directions : (N, 3) numpy.ndarray
36+
Orientations (normal vectors) of secondary sources
37+
weights : (N,) numpy.ndarray
38+
Weights of secondary sources
39+
2040
Example
2141
-------
2242
.. plot::
@@ -27,13 +47,12 @@ def linear(N, dx, center=[0, 0, 0], n0=[1, 0, 0]):
2747
plt.axis('equal')
2848
2949
"""
30-
center = np.squeeze(np.asarray(center, dtype=np.float64))
3150
positions = np.zeros((N, 3))
32-
positions[:, 1] = (np.arange(N) - N / 2 + 1 / 2) * dx
51+
positions[:, 1] = (np.arange(N) - N/2 + 1/2) * spacing
3352
positions, directions = _rotate_array(positions, [1, 0, 0], [1, 0, 0], n0)
34-
directions = np.tile(directions, (N, 1))
3553
positions += center
36-
weights = dx * np.ones(N)
54+
directions = np.tile(directions, (N, 1))
55+
weights = spacing * np.ones(N)
3756
return positions, directions, weights
3857

3958

@@ -50,7 +69,6 @@ def linear_nested(N, dx1, dx2, center=[0, 0, 0], n0=[1, 0, 0]):
5069
plt.axis('equal')
5170
5271
"""
53-
5472
# first segment
5573
x00, n00, a00 = linear(N//3, dx2, center=[0, -N//6*(dx1+dx2), 0])
5674
positions = x00
@@ -68,7 +86,6 @@ def linear_nested(N, dx1, dx2, center=[0, 0, 0], n0=[1, 0, 0]):
6886
# shift and rotate array
6987
positions, directions = _rotate_array(positions, directions, [1, 0, 0], n0)
7088
positions += center
71-
7289
return positions, directions, weights
7390

7491

@@ -93,14 +110,13 @@ def linear_random(N, dy1, dy2, center=[0, 0, 0], n0=[1, 0, 0]):
93110
positions[m, 1] = positions[m-1, 1] + dist[m-1]
94111
# weights of secondary sources
95112
weights = weights_linear(positions)
96-
# directions of scondary sources
113+
# directions of secondary sources
97114
directions = np.tile([1, 0, 0], (N, 1))
98-
# shift array to center
115+
# shift array to origin
99116
positions[:, 1] -= positions[-1, 1] / 2
100117
# shift and rotate array
101118
positions, directions = _rotate_array(positions, directions, [1, 0, 0], n0)
102119
positions += center
103-
104120
return positions, directions, weights
105121

106122

@@ -113,11 +129,11 @@ def circular(N, R, center=[0, 0, 0]):
113129
:context: close-figs
114130
115131
x0, n0, a0 = sfs.array.circular(16, 1)
116-
sfs.plot.loudspeaker_2d(x0, n0, a0)
132+
sfs.plot.loudspeaker_2d(x0, n0, a0, size=0.2, show_numbers=True)
117133
plt.axis('equal')
118134
119135
"""
120-
center = np.squeeze(np.asarray(center, dtype=np.float64))
136+
center = util.asarray_1d(center, dtype=np.float64)
121137
positions = np.tile(center, (N, 1))
122138
alpha = np.linspace(0, 2 * np.pi, N, endpoint=False)
123139
positions[:, 0] += R * np.cos(alpha)
@@ -138,11 +154,10 @@ def rectangular(Nx, dx, Ny, dy, center=[0, 0, 0], n0=None):
138154
:context: close-figs
139155
140156
x0, n0, a0 = sfs.array.rectangular(8, 0.2, 4, 0.2)
141-
sfs.plot.loudspeaker_2d(x0, n0, a0)
157+
sfs.plot.loudspeaker_2d(x0, n0, a0, show_numbers=True)
142158
plt.axis('equal')
143159
144160
"""
145-
146161
# left array
147162
x00, n00, a00 = linear(Ny, dy)
148163
positions = x00
@@ -167,15 +182,14 @@ def rectangular(Nx, dx, Ny, dy, center=[0, 0, 0], n0=None):
167182
positions = np.concatenate((positions, x00))
168183
directions = np.concatenate((directions, n00))
169184
weights = np.concatenate((weights, a00))
170-
# shift array to center
171-
positions -= np.asarray([Nx/2 * dx, 0, 0])
185+
# shift array to origin
186+
positions -= [Nx/2 * dx, 0, 0]
172187
# rotate array
173188
if n0 is not None:
174189
positions, directions = _rotate_array(positions, directions,
175190
[1, 0, 0], n0)
176191
# shift array to desired position
177-
positions += np.asarray(center)
178-
192+
positions += center
179193
return positions, directions, weights
180194

181195

@@ -213,7 +227,6 @@ def rounded_edge(Nxy, Nr, dx, center=[0, 0, 0], n0=None):
213227
plt.axis('equal')
214228
215229
"""
216-
217230
# radius of rounded edge
218231
Nr += 1
219232
R = 2/np.pi * Nr * dx
@@ -253,14 +266,12 @@ def rounded_edge(Nxy, Nr, dx, center=[0, 0, 0], n0=None):
253266
positions, directions = _rotate_array(positions, directions,
254267
[1, 0, 0], n0)
255268
# shift array to desired position
256-
positions += np.asarray(center)
257-
269+
positions += center
258270
return positions, directions, weights
259271

260272

261273
def planar(Ny, dy, Nz, dz, center=[0, 0, 0], n0=None):
262274
"""Planar secondary source distribtion."""
263-
center = np.squeeze(np.asarray(center, dtype=np.float64))
264275
# initialize vectors for later np.concatenate
265276
positions = np.zeros((1, 3))
266277
directions = np.zeros((1, 3))
@@ -277,14 +288,12 @@ def planar(Ny, dy, Nz, dz, center=[0, 0, 0], n0=None):
277288
positions, directions = _rotate_array(positions, directions,
278289
[1, 0, 0], n0)
279290
# shift array to desired position
280-
positions += np.asarray(center)
281-
291+
positions += center
282292
return positions, directions, weights
283293

284294

285295
def cube(Nx, dx, Ny, dy, Nz, dz, center=[0, 0, 0], n0=None):
286296
"""Cube shaped secondary source distribtion."""
287-
center = np.squeeze(np.asarray(center, dtype=np.float64))
288297
# left array
289298
x00, n00, a00 = planar(Ny, dy, Nz, dz)
290299
positions = x00
@@ -323,15 +332,14 @@ def cube(Nx, dx, Ny, dy, Nz, dz, center=[0, 0, 0], n0=None):
323332
positions = np.concatenate((positions, x00))
324333
directions = np.concatenate((directions, n00))
325334
weights = np.concatenate((weights, a00))
326-
# shift array to center
327-
positions -= np.asarray([Nx/2 * dx, 0, 0])
335+
# shift array to origin
336+
positions -= [Nx/2 * dx, 0, 0]
328337
# rotate array
329338
if n0 is not None:
330339
positions, directions = _rotate_array(positions, directions,
331340
[1, 0, 0], n0)
332341
# shift array to desired position
333-
positions += np.asarray(center)
334-
342+
positions += center
335343
return positions, directions, weights
336344

337345

@@ -344,9 +352,8 @@ def sphere_load(fname, radius, center=[0, 0, 0]):
344352
"""
345353
x0 = np.loadtxt(fname)
346354
weights = x0[:, 3]
347-
directions = - x0[:, 0:3]
348-
positions = center + radius * x0[:, 0:3]
349-
355+
directions = -x0[:, :3]
356+
positions = center + radius * x0[:, :3]
350357
return positions, directions, weights
351358

352359

@@ -366,8 +373,7 @@ def load(fname, center=[0, 0, 0], n0=None):
366373
positions, directions = _rotate_array(positions, directions,
367374
[1, 0, 0], n0)
368375
# shift array to desired position
369-
positions += np.asarray(center)
370-
376+
positions += center
371377
return positions, directions, weights
372378

373379

@@ -384,7 +390,6 @@ def weights_linear(positions):
384390
for m in range(1, N - 1):
385391
weights[m] = 0.5 * (dy[m-1] + dy[m])
386392
weights[-1] = dy[-1]
387-
388393
return np.abs(weights)
389394

390395

@@ -397,7 +402,7 @@ def weights_closed(positions):
397402
contour.
398403
399404
"""
400-
positions = np.asarray(positions)
405+
positions = util.asarray_of_rows(positions)
401406
if len(positions) == 0:
402407
weights = []
403408
elif len(positions) == 1:
@@ -406,7 +411,7 @@ def weights_closed(positions):
406411
successors = np.roll(positions, -1, axis=0)
407412
d = [np.linalg.norm(b - a) for a, b in zip(positions, successors)]
408413
weights = [0.5 * (a + b) for a, b in zip(d, d[-1:] + d)]
409-
return weights
414+
return np.array(weights)
410415

411416

412417
def _rotate_array(x0, n0, n1, n2):

sfs/plot.py

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def _register_coolwarm_clip():
2222
plt.cm.register_cmap(cmap=cmap)
2323

2424
_register_coolwarm_clip()
25+
del _register_coolwarm_clip
2526

2627

2728
def virtualsource_2d(xs, ns=None, type='point', ax=None):
@@ -70,16 +71,33 @@ def secondarysource_2d(x0, n0, grid=None):
7071
ax.add_artist(ss)
7172

7273

73-
def loudspeaker_2d(x0, n0, a0=None, size=0.08, index=False, grid=None):
74-
"""Draw loudspeaker symbols at given locations, angles."""
75-
x0 = np.asarray(x0)
76-
n0 = np.asarray(n0)
77-
patches = []
78-
fc = []
79-
if a0 is None:
80-
a0 = 0.5 * np.ones(len(x0))
81-
else:
82-
a0 = np.asarray(a0)
74+
def loudspeaker_2d(x0, n0, a0=0.5, size=0.08, show_numbers=False, grid=None,
75+
ax=None):
76+
"""Draw loudspeaker symbols at given locations and angles.
77+
78+
Parameters
79+
----------
80+
x0 : (N, 3) array_like
81+
Loudspeaker positions.
82+
n0 : (N, 3) or (3,) array_like
83+
Normal vector(s) of loudspeakers.
84+
a0 : float or (N,) array_like, optional
85+
Weighting factor(s) of loudspeakers.
86+
size : float, optional
87+
Size of loudspeakers in metres.
88+
show_numbers : bool, optional
89+
If ``True``, loudspeaker numbers are shown.
90+
grid : triple of numpy.ndarray, optional
91+
If specified, only loudspeakers within the `grid` are shown.
92+
ax : Axes object, optional
93+
The loudspeakers are plotted into this
94+
:class:`~matplotlib.axes.Axes` object or -- if not specified --
95+
into the current axes.
96+
97+
"""
98+
x0 = util.asarray_of_rows(x0)
99+
n0 = util.asarray_of_rows(n0)
100+
a0 = util.asarray_1d(a0).reshape(-1, 1)
83101

84102
# plot only secondary sources inside simulated area
85103
if grid is not None:
@@ -100,30 +118,25 @@ def loudspeaker_2d(x0, n0, a0=None, size=0.08, index=False, grid=None):
100118
coordinates = np.column_stack([coordinates, np.zeros(len(coordinates))])
101119
coordinates *= size
102120

103-
for x00, n00, a00 in zip(x0, n0, a0):
121+
patches = []
122+
for x00, n00 in util.broadcast_zip(x0, n0):
104123
# rotate and translate coordinates
105124
R = util.rotation_matrix([1, 0, 0], n00)
106125
transformed_coordinates = np.inner(coordinates, R) + x00
107126

108127
patches.append(PathPatch(Path(transformed_coordinates[:, :2], codes)))
109128

110-
# set facecolor
111-
fc.append((1-a00) * np.ones(3))
112-
113129
# add collection of patches to current axis
114-
p = PatchCollection(patches, edgecolor='0', facecolor=fc, alpha=1)
115-
ax = plt.gca()
130+
p = PatchCollection(patches, edgecolor='0', facecolor=np.tile(1 - a0, 3))
131+
if ax is None:
132+
ax = plt.gca()
116133
ax.add_collection(p)
117134

118-
# plot index of secondary source
119-
if index is True:
120-
idx = 1
121-
for (x00, n00) in zip(x0, n0):
122-
x = x00[0] - 0.3 * n00[0]
123-
y = x00[1] - 0.3 * n00[1]
124-
ax.text(x, y, idx, fontsize=9, horizontalalignment='center',
135+
if show_numbers:
136+
for idx, (x00, n00) in enumerate(util.broadcast_zip(x0, n0)):
137+
x, y = x00[:2] - 1.2 * size * n00[:2]
138+
ax.text(x, y, idx + 1, horizontalalignment='center',
125139
verticalalignment='center')
126-
idx += 1
127140

128141

129142
def _visible_secondarysources_2d(x0, n0, grid):

sfs/util.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
def rotation_matrix(n1, n2):
88
"""Compute rotation matrix for rotation from `n1` to `n2`."""
9-
n1 = np.asarray(n1)
10-
n2 = np.asarray(n2)
9+
n1 = asarray_1d(n1)
10+
n2 = asarray_1d(n2)
1111
# no rotation required
1212
if all(n1 == n2):
1313
return np.eye(3)
@@ -63,16 +63,33 @@ def asarray_1d(a, **kwargs):
6363
"""Squeeze the input and check if the result is one-dimensional.
6464
6565
Returns `a` converted to a :class:`numpy.ndarray` and stripped of
66-
all singleton dimensions. The result must have exactly one
67-
dimension. If not, an error is raised.
66+
all singleton dimensions. Scalars are "upgraded" to 1D arrays.
67+
The result must have exactly one dimension.
68+
If not, an error is raised.
6869
6970
"""
7071
result = np.squeeze(np.asarray(a, **kwargs))
71-
if result.ndim != 1:
72+
if result.ndim == 0:
73+
result = result.reshape((1,))
74+
elif result.ndim > 1:
7275
raise ValueError("array must be one-dimensional")
7376
return result
7477

7578

79+
def asarray_of_rows(a, **kwargs):
80+
"""Convert to 2D array, turn column vector into row vector.
81+
82+
Returns `a` converted to a :class:`numpy.ndarray` and stripped of
83+
all singleton dimensions. If the result has exactly one dimension,
84+
it is re-shaped into a 2D row vector.
85+
86+
"""
87+
result = np.squeeze(np.asarray(a, **kwargs))
88+
if result.ndim == 1:
89+
result = result.reshape(1, -1)
90+
return result
91+
92+
7693
def asarray_of_arrays(a, **kwargs):
7794
"""Convert the input to an array consisting of arrays.
7895
@@ -179,3 +196,8 @@ def level(p, grid, x):
179196
# p is normally squeezed, therefore we need only 2 dimensions:
180197
idx = idx[:p.ndim]
181198
return abs(p[idx])
199+
200+
201+
def broadcast_zip(*args):
202+
"""Broadcast arguments to the same shape and then use :func:`zip`."""
203+
return zip(*np.broadcast_arrays(*args))

0 commit comments

Comments
 (0)