Skip to content

Commit 75f353c

Browse files
committed
2.5D WFS referencing schemes line, circ, point
use new sfs.util.normalize_rows() remove colorbar_kwargs label, we will use the default handling, which will be adapted in near future xref handling as proposed by mgeier xref array handling nicer mgeier suggested to improve np.array([[0, 0, 0]]*array.x.shape[0]) and np.array([xref]*array.x.shape[0]) by a handling in sound_field() make xref = np.broadcast_to(xref, (N, 3)) handling simpler and more elegant as proposed by mgeier
1 parent cd74a17 commit 75f353c

File tree

4 files changed

+268
-2
lines changed

4 files changed

+268
-2
lines changed

doc/examples.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Examples
1313
.. nbgallery::
1414

1515
examples/sound-field-synthesis
16+
examples/wfs-referencing
1617
examples/modal-room-acoustics
1718
examples/mirror-image-source-model
1819
examples/animations-pulsating-sphere

doc/examples/wfs-referencing.ipynb

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# 2.5D WFS Referencing Schemes\n",
8+
"\n",
9+
"This notebook illustrates the usage of the SFS toolbox for the simulation of different 2.5D WFS referencing schemes.\n",
10+
"A dedicated referencing scheme allows correct amplitude alongside a reference contour within the listening area.\n",
11+
"For the theory please check\n",
12+
"Ch 3.1-3.3 in <cite data-cite=\"Start1997\">(Start1997)</cite>,\n",
13+
"Ch. 4.1.3 in <cite data-cite=\"Firtha2019\">(Firtha2019)</cite> and\n",
14+
"<cite data-cite=\"Firtha2017\">(Firtha2017)</cite>."
15+
]
16+
},
17+
{
18+
"cell_type": "code",
19+
"execution_count": null,
20+
"metadata": {},
21+
"outputs": [],
22+
"source": [
23+
"import matplotlib.pyplot as plt\n",
24+
"import numpy as np\n",
25+
"import sfs"
26+
]
27+
},
28+
{
29+
"cell_type": "markdown",
30+
"metadata": {},
31+
"source": [
32+
"## Circular loudspeaker arrays"
33+
]
34+
},
35+
{
36+
"cell_type": "code",
37+
"execution_count": null,
38+
"metadata": {},
39+
"outputs": [],
40+
"source": [
41+
"R = 1.5 # radius [m] of circular loudspeaker array\n",
42+
"N = 64 # loudspeakers\n",
43+
"array = sfs.array.circular(N=N, R=R)\n",
44+
"grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02)\n",
45+
"\n",
46+
"xs = -4, 0, 0 # virtual point source on negative x-axis\n",
47+
"wavelength = 1 / 4 # m"
48+
]
49+
},
50+
{
51+
"cell_type": "code",
52+
"execution_count": null,
53+
"metadata": {},
54+
"outputs": [],
55+
"source": [
56+
"def sound_field(d, selection, array, secondary_source, grid, xref):\n",
57+
" p = sfs.fd.synthesize(d, selection, array, secondary_source, grid=grid)\n",
58+
" fig, [ax_amp, ax_lvl] = plt.subplots(2, 1, sharex=True)\n",
59+
" fig.set_figheight(fig.get_figwidth() * 3/2)\n",
60+
" sfs.plot2d.amplitude(p, grid, vmax=2, vmin=-2, ax=ax_amp)\n",
61+
" sfs.plot2d.level(p, grid, vmax=6, vmin=-6, ax=ax_lvl)\n",
62+
" sfs.plot2d.level_contour(p, grid, levels=[0], colors='w', ax=ax_lvl)\n",
63+
" xref = np.broadcast_to(xref, array.x.shape)\n",
64+
" for ax in ax_amp, ax_lvl:\n",
65+
" sfs.plot2d.loudspeakers(array.x, array.n, selection, size=0.125, ax=ax)\n",
66+
" ax_lvl.scatter(*xref[selection, :2].T, marker='o', s=20, c='lightsalmon',\n",
67+
" zorder=3)\n",
68+
" plt.tight_layout()\n",
69+
" return p"
70+
]
71+
},
72+
{
73+
"cell_type": "code",
74+
"execution_count": null,
75+
"metadata": {},
76+
"outputs": [],
77+
"source": [
78+
"xs = sfs.util.asarray_of_rows(xs)\n",
79+
"frequency = sfs.default.c / wavelength # Hz\n",
80+
"omega = 2 * np.pi * frequency # rad/s\n",
81+
"normalize_gain = 4 * np.pi * np.linalg.norm(xs)"
82+
]
83+
},
84+
{
85+
"cell_type": "markdown",
86+
"metadata": {},
87+
"source": [
88+
"### Line as reference contour\n",
89+
"\n",
90+
"The reference contour is calculated according to eqs. (24), (31), (52) in <cite data-cite=\"Firtha2017\">(Firtha2017)</cite>. \n",
91+
"The code assumes a virtual point source on x-axis.\n",
92+
"The reference contour is a straight line on y-axis."
93+
]
94+
},
95+
{
96+
"cell_type": "code",
97+
"execution_count": null,
98+
"metadata": {},
99+
"outputs": [],
100+
"source": [
101+
"xref_line = 0\n",
102+
"cosbeta = (array.n @ [1, 0, 0]).reshape(-1, 1)\n",
103+
"xref = array.x + \\\n",
104+
" (xs - array.x) * (xref_line + R * cosbeta) / (xs[0, 0] + R * cosbeta)\n",
105+
"\n",
106+
"d, selection, secondary_source = sfs.fd.wfs.point_25d(\n",
107+
" omega, array.x, array.n, xs, xref=xref)\n",
108+
"p_line = sound_field(\n",
109+
" d * normalize_gain, selection, array, secondary_source, grid, xref)"
110+
]
111+
},
112+
{
113+
"cell_type": "markdown",
114+
"metadata": {},
115+
"source": [
116+
"The level plot includes a white 0 dB isobar curve.\n",
117+
"The orange-like dots represent the stationary phase points at which amplitude correct synthesis is to be expected.\n",
118+
"These dots shape the line reference contour.\n",
119+
"Note that the isobar curve is not perfectly aligned along line reference contour due to diffraction artifacts."
120+
]
121+
},
122+
{
123+
"cell_type": "markdown",
124+
"metadata": {},
125+
"source": [
126+
"### Circle as reference contour\n",
127+
"\n",
128+
"This reference contour is a circle with its origin at xs and a radius |xs|. This contour is obtained with more straightforward vector calculus than the previous example."
129+
]
130+
},
131+
{
132+
"cell_type": "code",
133+
"execution_count": null,
134+
"metadata": {},
135+
"outputs": [],
136+
"source": [
137+
"# reference contour is a circle with origin xs and radius |xs|\n",
138+
"xref_dist = np.linalg.norm(xs)\n",
139+
"# calc reference contour xref(x0), cf. [Firtha19, eq. (24), (31)]\n",
140+
"xref = xs + xref_dist * sfs.util.normalize_rows(array.x - xs)\n",
141+
"d, selection, secondary_source = sfs.fd.wfs.point_25d(\n",
142+
" omega, array.x, array.n, xs, xref=xref)\n",
143+
"p_circ = sound_field(\n",
144+
" d * normalize_gain, selection, array, secondary_source, grid, xref)"
145+
]
146+
},
147+
{
148+
"cell_type": "markdown",
149+
"metadata": {},
150+
"source": [
151+
"### Reference point"
152+
]
153+
},
154+
{
155+
"cell_type": "markdown",
156+
"metadata": {},
157+
"source": [
158+
"The default handling in\n",
159+
"`point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None)`\n",
160+
"uses just a reference point xref, and more specifically this default point is the origin of the coordinate system.\n",
161+
"This single point xref, the virtual source position xs and the loudspeaker array geometry together determine the reference contour without further user access to it.\n",
162+
"This handling is chosen due to convenience and practical relevance when working with circular loudspeaker arrays.\n",
163+
"\n",
164+
"The example below shows the resulting reference contour for the default case.\n",
165+
"In the example it looks similar to the line reference contour, but is in general not exactly the same.\n",
166+
"For example, please try a virtual point source that is far away from the array."
167+
]
168+
},
169+
{
170+
"cell_type": "code",
171+
"execution_count": null,
172+
"metadata": {},
173+
"outputs": [],
174+
"source": [
175+
"d, selection, secondary_source = sfs.fd.wfs.point_25d(\n",
176+
" omega, array.x, array.n, xs)\n",
177+
"p_point = sound_field(\n",
178+
" d * normalize_gain, selection, array, secondary_source,\n",
179+
" grid, [0, 0, 0])"
180+
]
181+
},
182+
{
183+
"cell_type": "markdown",
184+
"metadata": {},
185+
"source": [
186+
"Points with amplitude correct synthesis need to be stationary phase points, theoretically.\n",
187+
"Within the listening area, these points are found on rays that start at the virtual point source and intersect with active loudspeakers.\n",
188+
"The chosen points together shall shape a smooth contour, i.e. the reference contour.\n",
189+
"\n",
190+
"The example below shows a reference point xref that does not meet any ray (the gray lines in the level plot) alongside the stationary phase holds with its corresponding loudspeaker.\n",
191+
"\n",
192+
"The single point referencing scheme results in 0 dB isobar curve that closely passes the chosen xref point.\n",
193+
"In practice this typically works with sufficient precision once the position of xref is appropriately chosen (i.e. not too close, not too far, not to off-center from the active loudspeakers etc.)."
194+
]
195+
},
196+
{
197+
"cell_type": "code",
198+
"execution_count": null,
199+
"metadata": {},
200+
"outputs": [],
201+
"source": [
202+
"xref = 0, 0.1175, 0 # intentionally no stationary phase point\n",
203+
"# we don't forget to normalize the point source's amplitude\n",
204+
"# to this new reference point:\n",
205+
"normalize_gain = 4 * np.pi * np.linalg.norm(xs - xref)\n",
206+
"d, selection, secondary_source = sfs.fd.wfs.point_25d(\n",
207+
" omega, array.x, array.n, xs, xref=xref)\n",
208+
"p_point = sound_field(\n",
209+
" d * normalize_gain, selection, array, secondary_source,\n",
210+
" grid, xref)\n",
211+
"\n",
212+
"# plot stationary phase rays\n",
213+
"# one ray connects the virtual source with one activate loudspeaker\n",
214+
"spa = array.x + 3*R * sfs.util.normalize_rows(array.x - xs)\n",
215+
"plt.plot(\n",
216+
" np.vstack((array.x[selection, 0], spa[selection, 0])),\n",
217+
" np.vstack((array.x[selection, 1], spa[selection, 1])),\n",
218+
" color='gray')\n",
219+
"plt.xlim(-2, 2)\n",
220+
"plt.ylim(-2, 2);\n"
221+
]
222+
},
223+
{
224+
"cell_type": "markdown",
225+
"metadata": {},
226+
"source": [
227+
"A plane wave like sound field, e.g. by setting `xs = -100, 0, 0`, for all above examples reveals some further interesting implications of the different referencing schemes."
228+
]
229+
}
230+
],
231+
"metadata": {
232+
"kernelspec": {
233+
"display_name": "sfs",
234+
"language": "python",
235+
"name": "python3"
236+
},
237+
"language_info": {
238+
"codemirror_mode": {
239+
"name": "ipython",
240+
"version": 3
241+
},
242+
"file_extension": ".py",
243+
"mimetype": "text/x-python",
244+
"name": "python",
245+
"nbconvert_exporter": "python",
246+
"pygments_lexer": "ipython3",
247+
"version": "3.13.7"
248+
}
249+
},
250+
"nbformat": 4,
251+
"nbformat_minor": 2
252+
}

doc/references.bib

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,10 @@ @phdthesis{Schultz2016
9696
year = {2016},
9797
doi = {10.18453/rosdok_id00001765}
9898
}
99+
@phdthesis{Firtha2019,
100+
author = {Firtha, G.},
101+
title = {{A Generalized Wave Field Synthesis Framework with Application
102+
for Moving Virtual Sources}},
103+
school = {Budapest University of Technology and Economics},
104+
year = {2019}
105+
}

sfs/fd/wfs.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None):
205205
206206
is implemented.
207207
The theoretical link of `point_25d()` and `point_25d_legacy()` was
208-
introduced as *unified WFS framework* in :cite:`Firtha2017`.
208+
introduced as *unified WFS framework* in :cite:`Firtha2017`, :cite:`Firtha2019`.
209209
210210
Examples
211211
--------
@@ -217,6 +217,12 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None):
217217
normalize_gain = 4 * np.pi * np.linalg.norm(xs)
218218
plot(normalize_gain * d, selection, secondary_source)
219219
220+
.. nblinkgallery::
221+
:caption: Further Example
222+
:name: wfs-referencing-link-gallery
223+
224+
examples/wfs-referencing
225+
220226
"""
221227
x0 = _util.asarray_of_rows(x0)
222228
n0 = _util.asarray_of_rows(n0)
@@ -292,7 +298,7 @@ def point_25d_legacy(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None):
292298
\e{-\i\wc |\x_0-\x_\text{s}|}
293299
294300
The theoretical link of `point_25d()` and `point_25d_legacy()` was
295-
introduced as *unified WFS framework* in :cite:`Firtha2017`.
301+
introduced as *unified WFS framework* in :cite:`Firtha2017`, :cite:`Firtha2019`.
296302
Also cf. Eq. (2.145)-(2.147) :cite:`Schultz2016`.
297303
298304
Examples

0 commit comments

Comments
 (0)