diff --git a/data/arrays/example_array_4LS_2D.csv b/data/arrays/example_array_4LS_2D.csv new file mode 100644 index 0000000..4569574 --- /dev/null +++ b/data/arrays/example_array_4LS_2D.csv @@ -0,0 +1,4 @@ +1,0,0,-1,0,0,1 +0,1,0,0,-1,0,1 +-1,0,0,1,0,0,1 +0,-1,0,0,1,0,1 \ No newline at end of file diff --git a/data/arrays/example_array_6LS_3D.txt b/data/arrays/example_array_6LS_3D.txt new file mode 100644 index 0000000..c50f3f6 --- /dev/null +++ b/data/arrays/example_array_6LS_3D.txt @@ -0,0 +1,6 @@ +1 0 0 1 +-1 0 0 1 +0 1 0 1 +0 -1 0 1 +0 0 1 1 +0 0 -1 1 \ No newline at end of file diff --git a/data/arrays/university_rostock.csv b/data/arrays/wfs_university_rostock_2015.csv similarity index 100% rename from data/arrays/university_rostock.csv rename to data/arrays/wfs_university_rostock_2015.csv diff --git a/data/arrays/wfs_university_rostock_2018.csv b/data/arrays/wfs_university_rostock_2018.csv new file mode 100644 index 0000000..f3cbd1c --- /dev/null +++ b/data/arrays/wfs_university_rostock_2018.csv @@ -0,0 +1,64 @@ +1.8555,0.12942,1.6137,-1,0,0,0.1877 +1.8604,0.31567,1.6137,-1,0,0,0.2045 +1.8638,0.53832,1.6133,-1,0,0,0.22837 +1.8665,0.77237,1.6118,-1,0,0,0.24117 +1.8673,1.0206,1.6157,-1,0,0,0.24838 +1.8688,1.2691,1.6154,-1,0,0,0.23781 +1.8702,1.4962,1.6167,-1,0,0,0.20929 +1.8755,1.6876,1.6163,-1,0,0,0.22679 +1.6875,1.8702,1.6203,0,-1,0,0.22545 +1.4993,1.8843,1.6154,0,-1,0,0.21679 +1.2547,1.8749,1.6174,0,-1,0,0.23875 +1.022,1.8768,1.6184,0,-1,0,0.23992 +0.77488,1.8763,1.6175,0,-1,0,0.2349 +0.55221,1.8775,1.6177,0,-1,0,0.2327 +0.3095,1.8797,1.6157,0,-1,0,0.24573 +0.060789,1.882,1.6134,0,-1,0,0.21554 +-0.12151,1.8841,1.6101,0,-1,0,0.18685 +-0.31278,1.8791,1.613,0,-1,0,0.20506 +-0.53142,1.8855,1.6099,0,-1,0,0.22562 +-0.76382,1.8905,1.6061,0,-1,0,0.23945 +-1.0102,1.8888,1.6101,0,-1,0,0.25042 +-1.2646,1.8911,1.6086,0,-1,0,0.23947 +-1.4891,1.8936,1.607,0,-1,0,0.20807 +-1.6807,1.8964,1.6062,0,-1,0,0.22572 +-1.8625,1.7108,1.6075,1,0,0,0.22016 +-1.863,1.5303,1.6066,1,0,0,0.21877 +-1.8611,1.2733,1.6107,1,0,0,0.2448 +-1.8653,1.0408,1.6075,1,0,0,0.23885 +-1.8729,0.79578,1.6054,1,0,0,0.23437 +-1.8704,0.5722,1.6071,1,0,0,0.23219 +-1.881,0.33166,1.6053,1,0,0,0.24605 +-1.8783,0.080365,1.6075,1,0,0,0.21801 +-1.8781,-0.10434,1.6061,1,0,0,0.1852 +-1.8798,-0.28999,1.609,1,0,0,0.20278 +-1.8842,-0.50982,1.6095,1,0,0,0.22814 +-1.8911,-0.74608,1.6054,1,0,0,0.23945 +-1.8901,-0.98854,1.6102,1,0,0,0.24439 +-1.8928,-1.2348,1.6095,1,0,0,0.24209 +-1.8925,-1.4727,1.6117,1,0,0,0.21306 +-1.8939,-1.6609,1.6115,1,0,0,0.22209 +-1.7127,-1.8417,1.611,0,1,0,0.21959 +-1.5295,-1.8417,1.6129,0,1,0,0.21598 +-1.2809,-1.8485,1.6079,0,1,0,0.24212 +-1.0454,-1.8478,1.6094,0,1,0,0.2401 +-0.80072,-1.8512,1.609,0,1,0,0.23619 +-0.57305,-1.8524,1.6082,0,1,0,0.23437 +-0.33198,-1.8525,1.6074,0,1,0,0.24395 +-0.085164,-1.854,1.6085,0,1,0,0.21792 +0.10383,-1.8571,1.6082,0,1,0,0.18649 +0.28774,-1.8609,1.6061,0,1,0,0.20288 +0.50951,-1.8574,1.6049,0,1,0,0.22772 +0.74305,-1.8643,1.6034,0,1,0,0.23983 +0.989,-1.8695,1.6036,0,1,0,0.24802 +1.239,-1.8649,1.6041,0,1,0,0.24388 +1.4767,-1.8678,1.6054,0,1,0,0.20977 +1.6585,-1.8653,1.6059,0,1,0,0.22148 +1.8436,-1.6811,1.6054,-1,0,0,0.22264 +1.8563,-1.4974,1.6033,-1,0,0,0.21688 +1.8468,-1.248,1.6072,-1,0,0,0.24047 +1.85,-1.0167,1.6076,-1,0,0,0.23909 +1.8513,-0.76986,1.6101,-1,0,0,0.23739 +1.8585,-0.54207,1.6076,-1,0,0,0.23585 +1.8562,-0.29831,1.6107,-1,0,0,0.24122 +1.857,-0.059658,1.6121,-1,0,0,0.21387 diff --git a/doc/examples/sound_field_synthesis.py b/doc/examples/sound_field_synthesis.py index d680286..733ca2b 100644 --- a/doc/examples/sound_field_synthesis.py +++ b/doc/examples/sound_field_synthesis.py @@ -31,9 +31,11 @@ # === get secondary source positions === #array = sfs.array.linear(N, dx, center=[-1, 0, 0]) #array = sfs.array.linear_random(N, 0.2*dx, 5*dx) -array = sfs.array.rectangular(N, dx, orientation=sfs.util.direction_vector(0*np.pi/4)) -#array = sfs.array.circular(N, R) -#array = sfs.array.load('../../data/arrays/university_rostock.csv') +#array = sfs.array.rectangular(N, dx, orientation=sfs.util.direction_vector(0*np.pi/4)) +array = sfs.array.circular(N, R) +#array = sfs.array.load('../../data/arrays/wfs_university_rostock_2018.csv') +#array.x[:,2] = 0 # in wfs_university_rostock_2018.csv we encode absolute height +# which is not used here, we also could set the grid coordinate to z=1.615 m #array = sfs.array.planar(N, dx, orientation=sfs.util.direction_vector(np.radians(0), np.radians(180))) #array = sfs.array.cube(N, dx, orientation=sfs.util.direction_vector(0, np.pi/2)) diff --git a/sfs/array.py b/sfs/array.py index 91f3b76..d48b584 100644 --- a/sfs/array.py +++ b/sfs/array.py @@ -85,7 +85,7 @@ def as_secondary_source_distribution(arg, **kwargs): def linear(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): - """Linear secondary source distribution. + """Return linear, equidistantly sampled secondary source distribution. Parameters ---------- @@ -112,13 +112,15 @@ def linear(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): x0, n0, a0 = sfs.array.linear(16, 0.2, orientation=[0, -1, 0]) sfs.plot.loudspeaker_2d(x0, n0, a0) plt.axis('equal') + plt.xlabel('x / m') + plt.ylabel('y / m') """ return _linear_helper(np.arange(N) * spacing, center, orientation) def linear_diff(distances, center=[0, 0, 0], orientation=[1, 0, 0]): - """Linear secondary source distribution from a list of distances. + """Return linear secondary source distribution from a list of distances. Parameters ---------- @@ -141,6 +143,8 @@ def linear_diff(distances, center=[0, 0, 0], orientation=[1, 0, 0]): orientation=[0, -1, 0]) sfs.plot.loudspeaker_2d(x0, n0, a0) plt.axis('equal') + plt.xlabel('x / m') + plt.ylabel('y / m') """ distances = util.asarray_1d(distances) @@ -150,7 +154,7 @@ def linear_diff(distances, center=[0, 0, 0], orientation=[1, 0, 0]): def linear_random(N, min_spacing, max_spacing, center=[0, 0, 0], orientation=[1, 0, 0], seed=None): - """Randomly sampled linear array. + """Return randomly sampled linear array. Parameters ---------- @@ -174,9 +178,14 @@ def linear_random(N, min_spacing, max_spacing, center=[0, 0, 0], .. plot:: :context: close-figs - x0, n0, a0 = sfs.array.linear_random(12, 0.15, 0.4, orientation=[0, -1, 0]) + x0, n0, a0 = sfs.array.linear_random( + N=12, + min_spacing=0.15, max_spacing=0.4, + orientation=[0, -1, 0]) sfs.plot.loudspeaker_2d(x0, n0, a0) plt.axis('equal') + plt.xlabel('x / m') + plt.ylabel('y / m') """ r = np.random.RandomState(seed) @@ -185,7 +194,7 @@ def linear_random(N, min_spacing, max_spacing, center=[0, 0, 0], def circular(N, R, center=[0, 0, 0]): - """Circular secondary source distribution parallel to the xy-plane. + """Return circular secondary source distribution parallel to the xy-plane. Parameters ---------- @@ -209,6 +218,8 @@ def circular(N, R, center=[0, 0, 0]): x0, n0, a0 = sfs.array.circular(16, 1) sfs.plot.loudspeaker_2d(x0, n0, a0, size=0.2, show_numbers=True) plt.axis('equal') + plt.xlabel('x / m') + plt.ylabel('y / m') """ center = util.asarray_1d(center) @@ -225,7 +236,7 @@ def circular(N, R, center=[0, 0, 0]): def rectangular(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): - """Rectangular secondary source distribution. + """Return rectangular secondary source distribution. Parameters ---------- @@ -253,6 +264,8 @@ def rectangular(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): x0, n0, a0 = sfs.array.rectangular((4, 8), 0.2) sfs.plot.loudspeaker_2d(x0, n0, a0, show_numbers=True) plt.axis('equal') + plt.xlabel('x / m') + plt.ylabel('y / m') """ N1, N2 = (N, N) if np.isscalar(N) else N @@ -271,7 +284,7 @@ def rectangular(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): def rounded_edge(Nxy, Nr, dx, center=[0, 0, 0], orientation=[1, 0, 0]): - """Array along the xy-axis with rounded edge at the origin. + """Return SSD along the xy-axis with rounded edge at the origin. Parameters ---------- @@ -298,6 +311,8 @@ def rounded_edge(Nxy, Nr, dx, center=[0, 0, 0], orientation=[1, 0, 0]): x0, n0, a0 = sfs.array.rounded_edge(8, 5, 0.2) sfs.plot.loudspeaker_2d(x0, n0, a0) plt.axis('equal') + plt.xlabel('x / m') + plt.ylabel('y / m') """ # radius of rounded edge @@ -343,7 +358,7 @@ def rounded_edge(Nxy, Nr, dx, center=[0, 0, 0], orientation=[1, 0, 0]): def edge(Nxy, dx, center=[0, 0, 0], orientation=[1, 0, 0]): - """Array along the xy-axis with edge at the origin. + """Return SSD along the xy-axis with sharp edge at the origin. Parameters ---------- @@ -367,6 +382,8 @@ def edge(Nxy, dx, center=[0, 0, 0], orientation=[1, 0, 0]): x0, n0, a0 = sfs.array.edge(8, 0.2) sfs.plot.loudspeaker_2d(x0, n0, a0) plt.axis('equal') + plt.xlabel('x / m') + plt.ylabel('y / m') """ # array along y-axis @@ -393,7 +410,7 @@ def edge(Nxy, dx, center=[0, 0, 0], orientation=[1, 0, 0]): def planar(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): - """Planar secondary source distribtion. + """Return planar secondary source distribtion. Parameters ---------- @@ -412,6 +429,24 @@ def planar(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. + Examples + -------- + .. plot:: + :context: close-figs + + x0, n0, a0 = sfs.array.planar( + (4,3), 0.5, orientation=[0, 0, 1]) # 4 sources along y, 3 sources along x + x0, n0, a0 = sfs.array.planar( + (4,3), 0.5, orientation=[1, 0, 0]) # 4 sources along y, 3 sources along z + + x0, n0, a0 = sfs.array.planar( + (4,3), 0.5, orientation=[0, 1, 0]) # 4 sources along x, 3 sources along z + sfs.plot.loudspeaker_2d(x0, n0, a0) # plot the last ssd in 2D + plt.axis('equal') + plt.xlabel('x / m') + plt.ylabel('y / m') + + """ N1, N2 = (N, N) if np.isscalar(N) else N zcoordinates = np.arange(N2) * spacing @@ -426,7 +461,7 @@ def planar(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): def cube(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): - """Cube-shaped secondary source distribtion. + """Return cube-shaped secondary source distribtion. Parameters ---------- @@ -445,6 +480,20 @@ def cube(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. + Examples + -------- + .. plot:: + :context: close-figs + + x0, n0, a0 = sfs.array.cube( + N=2, spacing=0.5, + center=[0, 0, 0], orientation=[1, 0, 0]) + sfs.plot.loudspeaker_2d(x0, n0, a0) + plt.axis('equal') + plt.xlabel('x / m') + plt.ylabel('y / m') + plt.title('view onto xy-plane') + """ N1, N2, N3 = (N, N, N) if np.isscalar(N) else N offset1 = spacing * (N2 - 1) / 2 + spacing / np.sqrt(2) @@ -464,19 +513,48 @@ def cube(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): return SecondarySourceDistribution(positions, directions, weights) -def sphere_load(fname, radius, center=[0, 0, 0]): - """Spherical secondary source distribution loaded from datafile. +def sphere_load(file, radius, center=[0, 0, 0]): + """Load spherical secondary source distribution from file. - ASCII Format (see MATLAB SFS Toolbox) with 4 numbers (3 position, 1 - weight) per secondary source located on the unit circle. + ASCII Format (see MATLAB SFS Toolbox) with 4 numbers (3 for the cartesian + position vector, 1 for the integration weight) per secondary source located + on the unit circle which is resized by the given radius and shifted to the + given center. Returns ------- `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. + Examples + -------- + content of ``example_array_6LS_3D.txt``:: + + 1 0 0 1 + -1 0 0 1 + 0 1 0 1 + 0 -1 0 1 + 0 0 1 1 + 0 0 -1 1 + + corresponds to the `3-dimensional 6-point spherical 3-design + `_. + + .. plot:: + :context: close-figs + + x0, n0, a0 = sfs.array.sphere_load( + '../data/arrays/example_array_6LS_3D.txt', + radius=2, + center=[0, 0, 0]) + sfs.plot.loudspeaker_2d(x0, n0, a0, size=0.25) + plt.axis('equal') + plt.xlabel('x / m') + plt.ylabel('y / m') + plt.title('view onto xy-plane') + """ - data = np.loadtxt(fname) + data = np.loadtxt(file) positions, weights = data[:, :3], data[:, 3] normals = -positions positions *= radius @@ -484,19 +562,52 @@ def sphere_load(fname, radius, center=[0, 0, 0]): return SecondarySourceDistribution(positions, normals, weights) -def load(fname, center=[0, 0, 0], orientation=[1, 0, 0]): - """Load secondary source positions from datafile. +def load(file, center=[0, 0, 0], orientation=[1, 0, 0]): + """Load secondary source distribution from file. - Comma Seperated Values (CSV) format with 7 values - (3 positions, 3 normal vectors, 1 weight) per secondary source. + Comma Separated Values (CSV) format with 7 values + (3 for the cartesian position vector, 3 for the cartesian inward normal + vector, 1 for the integration weight) per secondary source. Returns ------- `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. + Examples + -------- + content of ``example_array_4LS_2D.csv``:: + + 1,0,0,-1,0,0,1 + 0,1,0,0,-1,0,1 + -1,0,0,1,0,0,1 + 0,-1,0,0,1,0,1 + + corresponds to 4 sources at 1, j, -1, -j in the complex plane. This setup + is typically used for Quadraphonic audio reproduction. + + .. plot:: + :context: close-figs + + x0, n0, a0 = sfs.array.load('../data/arrays/example_array_4LS_2D.csv') + sfs.plot.loudspeaker_2d(x0, n0, a0) + plt.axis('equal') + plt.xlabel('x / m') + plt.ylabel('y / m') + + .. plot:: + :context: close-figs + + x0, n0, a0 = sfs.array.load( + '../data/arrays/wfs_university_rostock_2018.csv') + sfs.plot.loudspeaker_2d(x0, n0, a0) + plt.axis('equal') + plt.xlabel('x / m') + plt.ylabel('y / m') + plt.title('top view of 64 channel WFS system at university of Rostock') + """ - data = np.loadtxt(fname, delimiter=',') + data = np.loadtxt(file, delimiter=',') positions, normals, weights = data[:, :3], data[:, 3:6], data[:, 6] positions, normals = _rotate_array(positions, normals, [1, 0, 0], orientation) @@ -515,8 +626,8 @@ def weights_midpoint(positions, closed): positions : (N, 3) array_like Sequence of secondary source positions. - .. note:: The loudspeaker positions have to be ordered on the - contour! + .. note:: The loudspeaker positions have to be ordered along the + contour. closed : bool ``True`` if the loudspeaker contour is closed. @@ -526,6 +637,14 @@ def weights_midpoint(positions, closed): (N,) numpy.ndarray Weights of secondary sources. + Examples + -------- + >>> import sfs + >>> x0, n0, a0 = sfs.array.circular(2**5, 1) + >>> a = sfs.array.weights_midpoint(x0, closed=True) + >>> max(abs(a0-a)) + 0.0003152601902411123 + """ positions = util.asarray_of_rows(positions) if closed: @@ -560,6 +679,27 @@ def _linear_helper(ycoordinates, center, orientation): def concatenate(*arrays): - """Concatenate `SecondarySourceDistribution` objects.""" + """Concatenate `SecondarySourceDistribution` objects. + + Returns + ------- + `SecondarySourceDistribution` + Positions, orientations and weights + of the concatenated secondary sources. + + Examples + -------- + .. plot:: + :context: close-figs + + ssd1 = sfs.array.edge(10, 0.2) + ssd2 = sfs.array.edge(20, 0.1, center=[2, 2, 0], orientation=[-1, 0, 0]) + x0, n0, a0 = sfs.array.concatenate(ssd1, ssd2) + sfs.plot.loudspeaker_2d(x0, n0, a0) + plt.axis('equal') + plt.xlabel('x / m') + plt.ylabel('y / m') + + """ return SecondarySourceDistribution._make(np.concatenate(i) for i in zip(*arrays))