bb9c969Add the cymbal problem: a listener near a rigid cymbal in free spaceJeremy Magland 1"""Plot a field saved by run.py (python plot.py results/<case>-<backend>): slices of Re(u) and level in dB,
2plus an animation of the real pressure field Re(u exp(-i omega t)) over one period. Works for the room
3and the cymbal problems."""
9b737b8GPU implementation of fmm3dbie's interior Helmholtz impedance solver, with a showcase room problemJeremy Magland 4
5import argparse
bb9c969Add the cymbal problem: a listener near a rigid cymbal in free spaceJeremy Magland 6import json
9b737b8GPU implementation of fmm3dbie's interior Helmholtz impedance solver, with a showcase room problemJeremy Magland 7import os
9import matplotlib
11matplotlib.use("Agg")
12import matplotlib.pyplot as plt
13import numpy as np
14from matplotlib.animation import FuncAnimation, PillowWriter
16SLICES = [("xy", "x", "y", 2), ("xz", "x", "z", 1), ("yz", "y", "z", 0)]
19def masked(a):
20 return np.ma.masked_invalid(a)
23def main():
24 ap = argparse.ArgumentParser()
25 ap.add_argument("out", help="a results directory written by run.py")
26 ap.add_argument("--frames", type=int, default=32)
27 args = ap.parse_args()
29 d = np.load(os.path.join(args.out, "field.npz"))
bb9c969Add the cymbal problem: a listener near a rigid cymbal in free spaceJeremy Magland 30 with open(os.path.join(args.out, "summary.json")) as f:
31 if json.load(f).get("problem", "room") == "cymbal":
32 return plot_cymbal(d, args)
9b737b8GPU implementation of fmm3dbie's interior Helmholtz impedance solver, with a showcase room problemJeremy Magland 33 g, x0, k = d["grid"], d["x0"], float(d["k"])
34 ext = [g[0], g[-1], g[0], g[-1]]
35 us = {n: d[f"u_{n}"] for n, *_ in SLICES}
37 # Color limits from the field more than half a wavelength from the source
38 A, B = np.meshgrid(g, g, indexing="xy")
39 far = []
40 for n, lx, ly, fixed in SLICES:
41 r = np.hypot(A - x0["xyz".index(lx)], B - x0["xyz".index(ly)])
42 u = us[n]
43 far.append(np.abs(u[np.isfinite(u) & (r > np.pi / k)]))
44 finite = np.concatenate(far)
45 umax = np.percentile(finite, 99)
46 ref = np.median(finite)
48 fig, axes = plt.subplots(2, 3, figsize=(13, 8.4), constrained_layout=True)
49 for col, (n, lx, ly, fixed) in enumerate(SLICES):
50 u = us[n]
51 ax = axes[0, col]
52 im0 = ax.imshow(masked(u.real), origin="lower", extent=ext, cmap="RdBu_r",
53 vmin=-umax, vmax=umax, interpolation="bilinear")
54 ax.set_title(f"Re u, {n}-plane at {'xyz'[fixed]} = {x0[fixed]:.2f}")
55 ax = axes[1, col]
56 with np.errstate(divide="ignore", invalid="ignore"):
57 level = 20 * np.log10(np.abs(u) / ref)
58 im1 = ax.imshow(masked(level), origin="lower", extent=ext, cmap="Blues_r",
59 vmin=-30, vmax=15, interpolation="bilinear")
60 ax.set_title(f"level (dB re median), {n}-plane")
61 for ax in axes[:, col]:
62 ax.plot(x0["xyz".index(lx)], x0["xyz".index(ly)], marker="o", ms=5,
63 mfc="none", mec="0.15", mew=1)
64 ax.set_xlabel(lx)
65 ax.set_ylabel(ly)
66 ax.set_aspect("equal")
67 fig.colorbar(im0, ax=axes[0, :], shrink=0.85, label="Re u")
68 fig.colorbar(im1, ax=axes[1, :], shrink=0.85, label="dB")
69 wl = 2 * np.pi / k
70 fig.suptitle(f"Point source in a rounded cubical room: k = {k:g} "
71 f"(room width = {2 * float(d['a']) / wl:.2f} wavelengths), "
72 f"wall admittance {-float(d['lam']):g}")
73 fig.savefig(os.path.join(args.out, "slices.png"), dpi=130)
74 plt.close(fig)
76 # Animation: Re(u e^{-i w t}) over one period on the three planes
77 fig, axes = plt.subplots(1, 3, figsize=(13, 4.6), constrained_layout=True)
78 ims = []
79 for ax, (n, lx, ly, fixed) in zip(axes, SLICES):
80 im = ax.imshow(masked(us[n].real), origin="lower", extent=ext, cmap="RdBu_r",
81 vmin=-umax, vmax=umax, interpolation="bilinear")
82 ax.set_title(f"{n}-plane")
83 ax.set_xlabel(lx)
84 ax.set_ylabel(ly)
85 ims.append(im)
86 fig.colorbar(ims[-1], ax=axes, shrink=0.85, label="Re(u e^{-iωt})")
87 title = fig.suptitle("")
89 def frame(i):
90 ph = np.exp(-2j * np.pi * i / args.frames)
91 for im, (n, *_) in zip(ims, SLICES):
92 im.set_data(masked((us[n] * ph).real))
93 title.set_text(f"pressure over one period, t/T = {i / args.frames:.2f}")
94 return ims
96 anim = FuncAnimation(fig, frame, frames=args.frames, blit=False)
97 anim.save(os.path.join(args.out, "animation.gif"), writer=PillowWriter(fps=12), dpi=80)
98 plt.close(fig)
99 print(f"wrote {args.out}/slices.png and {args.out}/animation.gif")
bb9c969Add the cymbal problem: a listener near a rigid cymbal in free spaceJeremy Magland 102CYMBAL_SLICES = ["xz", "yz", "xy"]
105def plot_cymbal(d, args):
106 """Slices through the evaluation box around the cymbal. u(x) is the pressure at the
107 listener due to a unit source at x (reciprocity). The level is shown relative to
108 the free field, 20 log10 |u / G|, which is 0 dB where the cymbal has no effect."""
109 x0, k, freq = d["x0"], float(d["k"]), float(d["freq"])
110 rz, R = d["outline"], float(d["radius"])
111 names = [n for n in CYMBAL_SLICES if f"u_{n}" in d]
112 panels = []
113 for n in names:
114 ga, gb = d[f"axes_{n}"]
115 fixed, val = d[f"plane_{n}"]
116 fixed = int(fixed)
117 ia, ib = [i for i in range(3) if i != fixed]
118 A, B = np.meshgrid(ga, gb, indexing="xy")
119 pts = np.zeros((3, A.size))
120 pts[ia], pts[ib], pts[fixed] = A.ravel(), B.ravel(), val
121 r = np.linalg.norm(pts - x0[:, None], axis=0)
122 G = (np.exp(1j * k * r) / (4 * np.pi * r)).reshape(A.shape)
123 panels.append((n, ga, gb, ia, ib, fixed, val, d[f"u_{n}"], G, r.reshape(A.shape)))
124 far = np.concatenate([np.abs(u[np.isfinite(u) & (r > np.pi / k)]) for *_, u, G, r in panels])
125 umax = np.percentile(far, 99)
127 def outline(ax, ia, ib, fixed, val):
128 if fixed == 2: # horizontal plane: the cymbal's rim, projected
129 t = np.linspace(0, 2 * np.pi, 200)
130 ax.plot(R * np.cos(t), R * np.sin(t), color="0.2", lw=0.8, ls="--")
131 elif abs(val) < R: # vertical plane: the cross section, at distance |val| from the axis
132 r, z = rz
133 m = r >= abs(val)
134 s = np.sqrt(np.maximum(r[m] ** 2 - val ** 2, 0))
135 for sign in (1, -1):
136 ax.fill(sign * s, z[m], color="0.25", lw=0)
138 fig, axes = plt.subplots(2, len(panels), figsize=(4.4 * len(panels), 8.4), constrained_layout=True,
139 squeeze=False)
140 for col, (n, ga, gb, ia, ib, fixed, val, u, G, r) in enumerate(panels):
141 ext = [ga[0], ga[-1], gb[0], gb[-1]]
142 ax = axes[0, col]
143 im0 = ax.imshow(masked(u.real), origin="lower", extent=ext, cmap="RdBu_r",
144 vmin=-umax, vmax=umax, interpolation="bilinear")
145 ax.set_title(f"Re u, {n}-plane at {'xyz'[fixed]} = {val:.2f} m")
146 ax = axes[1, col]
147 with np.errstate(divide="ignore", invalid="ignore"):
148 level = 20 * np.log10(np.abs(u) / np.abs(G))
149 im1 = ax.imshow(masked(level), origin="lower", extent=ext, cmap="PuOr_r",
150 vmin=-15, vmax=15, interpolation="bilinear")
151 ax.set_title(f"level re free field (dB), {n}-plane")
152 for ax in axes[:, col]:
153 outline(ax, ia, ib, fixed, val)
154 if abs(x0[fixed] - val) < 1e-9:
155 ax.plot(x0[ia], x0[ib], marker="o", ms=5, mfc="none", mec="0.15", mew=1)
156 ax.set_xlabel(f"{'xyz'[ia]} (m)")
157 ax.set_ylabel(f"{'xyz'[ib]} (m)")
158 ax.set_aspect("equal")
159 fig.colorbar(im0, ax=axes[0, :], shrink=0.85, label="Re u")
160 fig.colorbar(im1, ax=axes[1, :], shrink=0.85, label="dB")
161 wl = 2 * np.pi / k
162 fig.suptitle(f"Listener at ({x0[0]:.2f}, {x0[1]:.2f}, {x0[2]:.2f}) m near a rigid cymbal, "
163 f"{freq:g} Hz (diameter = {2 * R / wl:.2f} wavelengths)\nBy reciprocity u(x) is the "
164 f"pressure at the listener due to a unit source at x")
165 fig.savefig(os.path.join(args.out, "slices.png"), dpi=130)
166 plt.close(fig)
168 # Animation of the vertical plane through the listener
169 n, ga, gb, ia, ib, fixed, val, u, G, r = panels[0]
170 fig, ax = plt.subplots(figsize=(6, 5.4), constrained_layout=True)
171 im = ax.imshow(masked(u.real), origin="lower", extent=[ga[0], ga[-1], gb[0], gb[-1]], cmap="RdBu_r",
172 vmin=-umax, vmax=umax, interpolation="bilinear")
173 outline(ax, ia, ib, fixed, val)
174 ax.set_xlabel(f"{'xyz'[ia]} (m)")
175 ax.set_ylabel(f"{'xyz'[ib]} (m)")
176 fig.colorbar(im, ax=ax, shrink=0.85, label="Re(u e^{-iωt})")
177 title = ax.set_title("")
179 def frame(i):
180 im.set_data(masked((u * np.exp(-2j * np.pi * i / args.frames)).real))
181 title.set_text(f"{n}-plane, t/T = {i / args.frames:.2f}")
182 return [im]
184 anim = FuncAnimation(fig, frame, frames=args.frames, blit=False)
185 anim.save(os.path.join(args.out, "animation.gif"), writer=PillowWriter(fps=12), dpi=80)
186 plt.close(fig)
187 print(f"wrote {args.out}/slices.png and {args.out}/animation.gif")
9b737b8GPU implementation of fmm3dbie's interior Helmholtz impedance solver, with a showcase room problemJeremy Magland 190if __name__ == "__main__":
191 main()