From dcb138ebbea9f0455e280b82a0edb0b56c8b4f78 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 19 Jan 2019 23:52:42 +0200 Subject: [PATCH 001/101] Drop support for legacy Python 2.7 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e9a4e0c..a272b40 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - - "2.7" - "3.6" addons: apt: From 3c32e9f0874b29d4d23fbe8c86dac1e03723b89c Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 19 Jan 2019 23:53:02 +0200 Subject: [PATCH 002/101] Upgrade Python syntax with pyupgrade --py3-plus --- sfs/array.py | 2 +- sfs/mono/drivingfunction.py | 6 +++--- sfs/mono/source.py | 2 +- sfs/plot.py | 2 +- sfs/util.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sfs/array.py b/sfs/array.py index 9a787ec..1e63863 100644 --- a/sfs/array.py +++ b/sfs/array.py @@ -38,7 +38,7 @@ class ArrayData(namedtuple('ArrayData', 'x n a')): def __repr__(self): return 'ArrayData(\n' + ',\n'.join( - ' {0}={1}'.format(name, repr(data).replace('\n', '\n ')) + ' {}={}'.format(name, repr(data).replace('\n', '\n ')) for name, data in zip('xna', self)) + ')' def take(self, indices): diff --git a/sfs/mono/drivingfunction.py b/sfs/mono/drivingfunction.py index 4362f1e..af7f3df 100644 --- a/sfs/mono/drivingfunction.py +++ b/sfs/mono/drivingfunction.py @@ -51,7 +51,7 @@ def _wfs_point(omega, x0, n0, xs, c=None): def wfs_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): - """Point source by 2.5-dimensional WFS. + r"""Point source by 2.5-dimensional WFS. :: @@ -96,7 +96,7 @@ def _wfs_plane(omega, x0, n0, n=[0, 1, 0], c=None): def wfs_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, omalias=None): - """Plane wave by 2.5-dimensional WFS. + r"""Plane wave by 2.5-dimensional WFS. :: @@ -140,7 +140,7 @@ def _wfs_focused(omega, x0, n0, xs, c=None): def wfs_25d_focused(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): - """Focused source by 2.5-dimensional WFS. + r"""Focused source by 2.5-dimensional WFS. :: diff --git a/sfs/mono/source.py b/sfs/mono/source.py index 4aba9ea..e18370e 100644 --- a/sfs/mono/source.py +++ b/sfs/mono/source.py @@ -165,7 +165,7 @@ def point_averaged_intensity(omega, x0, n0, grid, c=None): def point_dipole(omega, x0, n0, grid, c=None): - """Point source with dipole characteristics. + r"""Point source with dipole characteristics. Parameters ---------- diff --git a/sfs/plot.py b/sfs/plot.py index 3d4e5be..c769fe5 100644 --- a/sfs/plot.py +++ b/sfs/plot.py @@ -394,7 +394,7 @@ def vectors(v, grid, cmap='blacktransparent', headlength=3, headaxislength=2.5, def add_colorbar(im, aspect=20, pad=0.5, **kwargs): - """Add a vertical color bar to a plot. + r"""Add a vertical color bar to a plot. Parameters ---------- diff --git a/sfs/util.py b/sfs/util.py index 73fa5bb..13410f2 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -431,7 +431,7 @@ def __getitem__(self, index): def __repr__(self): return 'XyzComponents(\n' + ',\n'.join( - ' {0}={1}'.format(name, repr(data).replace('\n', '\n ')) + ' {}={}'.format(name, repr(data).replace('\n', '\n ')) for name, data in zip('xyz', self)) + ')' def make_property(index, doc): From 84a688beb0376378769b63a2dabbd5925608f706 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 19 Jan 2019 23:54:19 +0200 Subject: [PATCH 003/101] Add Trove classifiers to clarify support on PyPI --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index 79cd4a5..a23092d 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,10 @@ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3 :: Only", "Topic :: Scientific/Engineering", ], zip_safe=True, From de276ee4b006b1f207a0692c1cfda3ecbd9b38fd Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 19 Jan 2019 23:55:15 +0200 Subject: [PATCH 004/101] Add python_requires to help pip --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index a23092d..9a7d870 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ keywords="audio SFS WFS Ambisonics".split(), url="http://github.com/sfstoolbox/", platforms='any', + python_requires='>=3.6', classifiers=[ "Development Status :: 3 - Alpha", "License :: OSI Approved :: MIT License", From 30efc1467b31b24f53148c26cb5f747c35169fc8 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 19 Jan 2019 23:56:15 +0200 Subject: [PATCH 005/101] The future is now --- sfs/array.py | 1 - sfs/plot.py | 1 - sfs/time/drivingfunction.py | 1 - sfs/time/soundfield.py | 1 - sfs/time/source.py | 1 - sfs/util.py | 1 - 6 files changed, 6 deletions(-) diff --git a/sfs/array.py b/sfs/array.py index 1e63863..7697fa1 100644 --- a/sfs/array.py +++ b/sfs/array.py @@ -12,7 +12,6 @@ :members: take """ -from __future__ import division # for Python 2.x from collections import namedtuple import numpy as np from . import util diff --git a/sfs/plot.py b/sfs/plot.py index c769fe5..b8411c2 100644 --- a/sfs/plot.py +++ b/sfs/plot.py @@ -1,5 +1,4 @@ """Plot sound fields etc.""" -from __future__ import division import matplotlib.pyplot as plt from matplotlib import __version__ as matplotlib_version from matplotlib.patches import PathPatch diff --git a/sfs/time/drivingfunction.py b/sfs/time/drivingfunction.py index 04922bd..a4e5c28 100644 --- a/sfs/time/drivingfunction.py +++ b/sfs/time/drivingfunction.py @@ -3,7 +3,6 @@ .. include:: math-definitions.rst """ -from __future__ import division import numpy as np from numpy.core.umath_tests import inner1d # element-wise inner product from .. import defs diff --git a/sfs/time/soundfield.py b/sfs/time/soundfield.py index ff6d32d..d81888e 100644 --- a/sfs/time/soundfield.py +++ b/sfs/time/soundfield.py @@ -1,6 +1,5 @@ """Compute sound field.""" -from __future__ import division from .. import util from .. import defs from .source import point diff --git a/sfs/time/source.py b/sfs/time/source.py index 0101daf..994e15f 100644 --- a/sfs/time/source.py +++ b/sfs/time/source.py @@ -6,7 +6,6 @@ """ -from __future__ import division import numpy as np from .. import util from .. import defs diff --git a/sfs/util.py b/sfs/util.py index 13410f2..006bf47 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -4,7 +4,6 @@ """ -from __future__ import division import collections import numpy as np from scipy.special import spherical_jn, spherical_yn From 20a3c07b99ea30be038abd8c520ab6a3ddd08718 Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 22 Jan 2019 21:07:46 +0200 Subject: [PATCH 006/101] Test on Python 3.5 and 3.7 --- .travis.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a272b40..05d7c9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,10 @@ language: python -python: - - "3.6" +matrix: + include: + - python: "3.5" + - python: "3.6" + - python: "3.7" + dist: xenial # 3.7 requires Xenial on Travis addons: apt: packages: From 7e54eaff7b04e3ccb4b51bf5b7ec8e9693ffd5f4 Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 22 Jan 2019 21:10:09 +0200 Subject: [PATCH 007/101] Update python_requires to 3.5+ --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9a7d870..0fedaa1 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ keywords="audio SFS WFS Ambisonics".split(), url="http://github.com/sfstoolbox/", platforms='any', - python_requires='>=3.6', + python_requires='>=3.5', classifiers=[ "Development Status :: 3 - Alpha", "License :: OSI Approved :: MIT License", From 17337f22fe4ea1a0f47e81befe514ca88239ff0e Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 22 Jan 2019 21:10:53 +0200 Subject: [PATCH 008/101] Add Python 3.5 Trovel classifier --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 0fedaa1..fc2bee5 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3 :: Only", From 4e0ae6e602d6561e4aa753a4abe985db1ab700bd Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sun, 30 Dec 2018 17:29:30 +0100 Subject: [PATCH 009/101] DOC: Correct return type: ndarray instead of XyzComponents --- sfs/mono/source.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sfs/mono/source.py b/sfs/mono/source.py index e18370e..67ce330 100644 --- a/sfs/mono/source.py +++ b/sfs/mono/source.py @@ -49,7 +49,7 @@ def point(omega, x0, n0, grid, c=None): Returns ------- - `XyzComponents` + numpy.ndarray Sound pressure at positions given by *grid*. Notes @@ -595,7 +595,7 @@ def plane(omega, x0, n0, grid, c=None): Returns ------- - `XyzComponents` + numpy.ndarray Sound pressure at positions given by *grid*. Notes From b4bce72dffbc61232de93ceab2c7f38d1d3b670b Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 23 Jun 2018 22:56:32 +0200 Subject: [PATCH 010/101] DOC: Use date of current commit instead of current date --- doc/conf.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index ecf342c..81003bf 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -115,6 +115,11 @@ #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' +try: + today = check_output(['git', 'show', '-s', '--format=%ad', '--date=short']) + today = today.decode().strip() +except Exception: + today = '' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. From b676d2f119ce83a8e1c6ea34a5e11b78278027e5 Mon Sep 17 00:00:00 2001 From: Jeremy Aweda Date: Mon, 8 Oct 2018 15:39:28 +0200 Subject: [PATCH 011/101] Test for util.db, util.direction_vector This is a part of #61 --- sfs/util.py | 2 +- tests/test_util.py | 62 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/sfs/util.py b/sfs/util.py index 006bf47..f91c4a6 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -361,7 +361,7 @@ def db(x, power=False): """ with np.errstate(divide='ignore'): - return 10 if power else 20 * np.log10(np.abs(x)) + return (10 if power else 20) * np.log10(np.abs(x)) class XyzComponents(np.ndarray): diff --git a/tests/test_util.py b/tests/test_util.py index 89ff89d..31b617e 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -4,14 +4,15 @@ import sfs cart_sph_data = [ - ((1, 1, 1), (np.pi/4, np.arccos(1/np.sqrt(3)), np.sqrt(3))), - ((-1, 1, 1), (np.arctan2(1, -1), np.arccos(1/np.sqrt(3)), np.sqrt(3))), - ((1, -1, 1), (-np.pi/4, np.arccos(1/np.sqrt(3)), np.sqrt(3))), - ((-1, -1, 1), (np.arctan2(-1, -1), np.arccos(1/np.sqrt(3)), np.sqrt(3))), - ((1, 1, -1), (np.pi/4, np.arccos(-1/np.sqrt(3)), np.sqrt(3))), - ((-1, 1, -1), (np.arctan2(1, -1), np.arccos(-1/np.sqrt(3)), np.sqrt(3))), - ((1, -1, -1), (-np.pi/4, np.arccos(-1/np.sqrt(3)), np.sqrt(3))), - ((-1, -1, -1), (np.arctan2(-1, -1), np.arccos(-1/np.sqrt(3)), np.sqrt(3))), + ((1, 1, 1), (np.pi / 4, np.arccos(1 / np.sqrt(3)), np.sqrt(3))), + ((-1, 1, 1), (np.arctan2(1, -1), np.arccos(1 / np.sqrt(3)), np.sqrt(3))), + ((1, -1, 1), (-np.pi / 4, np.arccos(1 / np.sqrt(3)), np.sqrt(3))), + ((-1, -1, 1), (np.arctan2(-1, -1), np.arccos(1 / np.sqrt(3)), np.sqrt(3))), + ((1, 1, -1), (np.pi / 4, np.arccos(-1 / np.sqrt(3)), np.sqrt(3))), + ((-1, 1, -1), (np.arctan2(1, -1), np.arccos(-1 / np.sqrt(3)), np.sqrt(3))), + ((1, -1, -1), (-np.pi / 4, np.arccos(-1 / np.sqrt(3)), np.sqrt(3))), + ((-1, -1, -1), (np.arctan2(-1, -1), + np.arccos(-1 / np.sqrt(3)), np.sqrt(3))), ] @@ -27,3 +28,48 @@ def test_sph2cart(coord, polar): alpha, beta, r = polar b = sfs.util.sph2cart(alpha, beta, r) assert_allclose(b, coord) + + +direction_vector_data = [ + ((np.pi / 4, np.pi / 4), (0.5, 0.5, np.sqrt(2) / 2)), + ((3 * np.pi / 4, 3 * np.pi / 4), (-1 / 2, 1 / 2, -np.sqrt(2) / 2)), + ((3 * np.pi / 4, -3 * np.pi / 4), (1 / 2, -1 / 2, -np.sqrt(2) / 2)), + ((np.pi / 4, -np.pi / 4), (-1 / 2, -1 / 2, np.sqrt(2) / 2)), + ((-np.pi / 4, np.pi / 4), (1 / 2, -1 / 2, np.sqrt(2) / 2)), + ((-3 * np.pi / 4, 3 * np.pi / 4), (-1 / 2, -1 / 2, -np.sqrt(2) / 2)), + ((-3 * np.pi / 4, -3 * np.pi / 4), (1 / 2, 1 / 2, -np.sqrt(2) / 2)), + ((-np.pi / 4, -np.pi / 4), (-1 / 2, 1 / 2, np.sqrt(2) / 2)), +] + + +@pytest.mark.parametrize('input, vector', direction_vector_data) +def test_direction_vector(input, vector): + alpha, beta = input + c = sfs.util.direction_vector(alpha, beta) + assert_allclose(c, vector) + + +db_data = [ + (0, -np.inf), + (1, 0), + (10, 10), + (10 * 2, (10 + 3.010299956639813)), + (10 * 10, (10 + 10)), + (10 * 3, (10 + 4.771212547196624)), + (10 * 4, (10 + 6.02059991327962)), + (10 * 0.5, (10 - 3.01029995663981198)), + (10 * 0.1, (10 - 10)), + (10 * 0.25, (10 - 6.02059991327962396)) + ] + + +@pytest.mark.parametrize('linear, decibel', db_data) +def test_db_amplitude(linear, decibel): + d = sfs.util.db(linear, True) + assert_allclose(d, decibel) + + +@pytest.mark.parametrize('linear, decibel', db_data) +def test_db_power(linear, decibel): + d = sfs.util.db(linear) + assert_allclose(d, 2 * decibel) From 059410f2aab46e83f8b2bf5898a9b17c346e6ed4 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 5 Feb 2019 20:33:36 +0100 Subject: [PATCH 012/101] TST: A little update to the dB tests --- tests/test_util.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/tests/test_util.py b/tests/test_util.py index 31b617e..d46fd76 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -51,25 +51,20 @@ def test_direction_vector(input, vector): db_data = [ (0, -np.inf), + (0.5, -3.01029995663981), (1, 0), + (2, 3.01029995663981), (10, 10), - (10 * 2, (10 + 3.010299956639813)), - (10 * 10, (10 + 10)), - (10 * 3, (10 + 4.771212547196624)), - (10 * 4, (10 + 6.02059991327962)), - (10 * 0.5, (10 - 3.01029995663981198)), - (10 * 0.1, (10 - 10)), - (10 * 0.25, (10 - 6.02059991327962396)) - ] +] -@pytest.mark.parametrize('linear, decibel', db_data) -def test_db_amplitude(linear, decibel): - d = sfs.util.db(linear, True) - assert_allclose(d, decibel) +@pytest.mark.parametrize('linear, power_db', db_data) +def test_db_amplitude(linear, power_db): + d = sfs.util.db(linear) + assert_allclose(d, power_db * 2) -@pytest.mark.parametrize('linear, decibel', db_data) -def test_db_power(linear, decibel): - d = sfs.util.db(linear) - assert_allclose(d, 2 * decibel) +@pytest.mark.parametrize('linear, power_db', db_data) +def test_db_power(linear, power_db): + d = sfs.util.db(linear, power=True) + assert_allclose(d, power_db) From 0a7e01f0eb42291cb8e8e5c59f42c07d9be05645 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 11 Feb 2019 12:16:44 +0100 Subject: [PATCH 013/101] DOC: convert image source example script to a Jupyter notebook --- doc/examples/index.rst | 1 + doc/examples/mirror-image-source-model.ipynb | 169 +++++++++++++++++++ doc/examples/mirror_image_source_model.py | 51 ------ 3 files changed, 170 insertions(+), 51 deletions(-) create mode 100644 doc/examples/mirror-image-source-model.ipynb delete mode 100644 doc/examples/mirror_image_source_model.py diff --git a/doc/examples/index.rst b/doc/examples/index.rst index b2f707d..c737441 100644 --- a/doc/examples/index.rst +++ b/doc/examples/index.rst @@ -20,3 +20,4 @@ Or Jupyter notebooks, which are also available online as interactive examples: :maxdepth: 1 modal-room-acoustics + mirror-image-source-model diff --git a/doc/examples/mirror-image-source-model.ipynb b/doc/examples/mirror-image-source-model.ipynb new file mode 100644 index 0000000..eb61663 --- /dev/null +++ b/doc/examples/mirror-image-source-model.ipynb @@ -0,0 +1,169 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Mirror Image Sources and the Sound Field in a Rectangular Room" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import sfs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "L = 2, 2.7, 3 # room dimensions\n", + "x0 = 1.2, 1.7, 1.5 # source position\n", + "max_order = 2 # maximum order of image sources\n", + "coeffs = .8, .8, .6, .6, .7, .7 # wall reflection coefficients" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2D Mirror Image Sources" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "xs, wall_count = sfs.util.image_sources_for_box(x0[0:2], L[0:2], max_order)\n", + "source_strength = np.prod(coeffs[0:4]**wall_count, axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib.patches import Rectangle" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "ax.scatter(*xs.T, source_strength * 20)\n", + "ax.add_patch(Rectangle((0, 0), L[0], L[1], fill=False))\n", + "ax.set_xlabel('x / m')\n", + "ax.set_ylabel('y / m')\n", + "ax.axis('equal');" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Monochromatic Sound Field" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "omega = 2 * np.pi * 1000 # angular frequency" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "grid = sfs.util.xyz_grid([0, L[0]], [0, L[1]], 1.5, spacing=0.02)\n", + "P = sfs.mono.source.point_image_sources(omega, x0, [1, 0, 0], grid, L,\n", + " max_order, coeffs=coeffs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sfs.plot.soundfield(P, grid, xnorm=[L[0]/2, L[1]/2, L[2]/2]);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Spatio-temporal Impulse Response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fs = 44100 # sample rate\n", + "signal = [1, 0, 0], fs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "grid = sfs.util.xyz_grid([0, L[0]], [0, L[1]], 1.5, spacing=0.005)\n", + "p = sfs.time.source.point_image_sources(x0, signal, 0.004, grid, L, max_order,\n", + " coeffs=coeffs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sfs.plot.level(p, grid)\n", + "sfs.plot.virtualsource_2d(x0)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.2+" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/doc/examples/mirror_image_source_model.py b/doc/examples/mirror_image_source_model.py deleted file mode 100644 index 4c8148f..0000000 --- a/doc/examples/mirror_image_source_model.py +++ /dev/null @@ -1,51 +0,0 @@ -""" Computes the mirror image sources and the sound field in a rectangular - room -""" - -import numpy as np -import sfs -import matplotlib.pyplot as plt -from matplotlib.patches import Rectangle - - -L = 2, 2.7, 3 # room dimensions -x0 = 1.2, 1.7, 1.5 # source position -max_order = 2 # maximum order of image sources -coeffs = .8, .8, .6, .6, .7, .7 # wall reflection coefficients -omega = 2*np.pi*1000 # angular frequency of monocromatic sound field -fs = 44100 # sample rate for boadband response -signal = ([1, 0, 0], fs) # signal for broadband response - - -# get 2D mirror image sources and their strength -xs, wall_count = sfs.util.image_sources_for_box(x0[0:2], L[0:2], max_order) -source_strength = np.prod(coeffs[0:4]**wall_count, axis=1) -# plot mirror image sources -plt.figure() -plt.scatter(*xs.T, source_strength*20) -plt.gca().add_patch(Rectangle((0, 0), L[0], L[1], fill=False)) -plt.xlabel('x / m') -plt.ylabel('y / m') -plt.savefig('image_source_positions.png') - - -# compute monochromatic sound field -grid = sfs.util.xyz_grid([0, L[0]], [0, L[1]], 1.5, spacing=0.02) -P = sfs.mono.source.point_image_sources(omega, x0, [1, 0, 0], grid, L, - max_order, coeffs=coeffs) -# plot monocromatic sound field -plt.figure() -sfs.plot.soundfield(P, grid, xnorm=[L[0]/2, L[1]/2, L[2]/2]) -sfs.plot.virtualsource_2d(x0) -plt.savefig('point_image_sources_mono.png') - - -# compute spatio-temporal impulse response -grid = sfs.util.xyz_grid([0, L[0]], [0, L[1]], 1.5, spacing=0.005) -p = sfs.time.source.point_image_sources(x0, signal, 0.004, grid, L, max_order, - coeffs=coeffs) -# plot spatio-temporal impulse response -plt.figure() -sfs.plot.level(p, grid) -sfs.plot.virtualsource_2d(x0) -plt.savefig('point_image_sources_time_domain.png') From b19acf208920c4a5133457e4555837a9a6a12efc Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 11 Feb 2019 20:27:25 +0100 Subject: [PATCH 014/101] DOC: Add plots in the API docs for drivingfunction --- sfs/mono/drivingfunction.py | 115 ++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/sfs/mono/drivingfunction.py b/sfs/mono/drivingfunction.py index af7f3df..15f05d6 100644 --- a/sfs/mono/drivingfunction.py +++ b/sfs/mono/drivingfunction.py @@ -2,6 +2,34 @@ .. include:: math-definitions.rst +.. plot:: + :context: reset + + import matplotlib.pyplot as plt + import numpy as np + import sfs + + plt.rcParams['figure.figsize'] = 6, 6 + + xs = -1.5, 1.5, 0 + xs_focused = -0.5, 0.5, 0 + # normal vector for plane wave: + npw = sfs.util.direction_vector(np.radians(-45)) + # normal vector for focused source: + ns = sfs.util.direction_vector(np.radians(-45)) + f = 300 # Hz + omega = 2 * np.pi * f + R = 1.5 # Radius of circular loudspeaker array + + grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02) + + x0, n0, a0 = sfs.array.circular(N=32, R=R) + + def plot(d, selected): + p = sfs.mono.synthesized.generic(omega, x0, n0, d * selected * a0 , grid) + sfs.plot.soundfield(p, grid) + sfs.plot.loudspeaker_2d(x0, n0, selected * a0, size=0.15) + """ import numpy as np @@ -18,6 +46,15 @@ def wfs_2d_line(omega, x0, n0, xs, c=None): D(x0,k) = j/2 k (x0-xs) n0 / |x0-xs| * H1(k |x0-xs|) + Examples + -------- + .. plot:: + :context: close-figs + + d = sfs.mono.drivingfunction.wfs_2d_line(omega, x0, n0, xs) + a = sfs.mono.drivingfunction.source_selection_line(n0, x0, xs) + plot(d, a) + """ x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) @@ -37,6 +74,15 @@ def _wfs_point(omega, x0, n0, xs, c=None): D(x0,k) = j k ------------- e^(-j k |x0-xs|) |x0-xs|^(3/2) + Examples + -------- + .. plot:: + :context: close-figs + + d = sfs.mono.drivingfunction.wfs_3d_point(omega, x0, n0, xs) + a = sfs.mono.drivingfunction.source_selection_point(n0, x0, xs) + plot(d, a) + """ x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) @@ -59,6 +105,15 @@ def wfs_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): D(x0,k) = \|j k |xref-x0| ------------- e^(-j k |x0-xs|) |x0-xs|^(3/2) + Examples + -------- + .. plot:: + :context: close-figs + + d = sfs.mono.drivingfunction.wfs_25d_point(omega, x0, n0, xs) + a = sfs.mono.drivingfunction.source_selection_point(n0, x0, xs) + plot(d, a) + """ x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) @@ -83,6 +138,15 @@ def _wfs_plane(omega, x0, n0, n=[0, 1, 0], c=None): D(x0,k) = j k n n0 e^(-j k n x0) + Examples + -------- + .. plot:: + :context: close-figs + + d = sfs.mono.drivingfunction.wfs_3d_plane(omega, x0, n0, npw) + a = sfs.mono.drivingfunction.source_selection_plane(n0, npw) + plot(d, a) + """ x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) @@ -103,6 +167,15 @@ def wfs_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, ____________ D_2.5D(x0,w) = \|j k |xref-x0| n n0 e^(-j k n x0) + Examples + -------- + .. plot:: + :context: close-figs + + d = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw) + a = sfs.mono.drivingfunction.source_selection_plane(n0, npw) + plot(d, a) + """ x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) @@ -126,6 +199,15 @@ def _wfs_focused(omega, x0, n0, xs, c=None): D(x0,k) = j k ------------- e^(j k |x0-xs|) |x0-xs|^(3/2) + Examples + -------- + .. plot:: + :context: close-figs + + d = sfs.mono.drivingfunction.wfs_3d_focused(omega, x0, n0, xs_focused) + a = sfs.mono.drivingfunction.source_selection_focused(ns, x0, xs_focused) + plot(d, a) + """ x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) @@ -148,6 +230,15 @@ def wfs_25d_focused(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): D(x0,w) = \|j k |xref-x0| ------------- e^(j k |x0-xs|) |x0-xs|^(3/2) + Examples + -------- + .. plot:: + :context: close-figs + + d = sfs.mono.drivingfunction.wfs_25d_focused(omega, x0, n0, xs_focused) + a = sfs.mono.drivingfunction.source_selection_focused(ns, x0, xs_focused) + plot(d, a) + """ x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) @@ -248,6 +339,14 @@ def nfchoa_2d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): See http://sfstoolbox.org/#equation-D.nfchoa.pw.2D. + Examples + -------- + .. plot:: + :context: close-figs + + d = sfs.mono.drivingfunction.nfchoa_2d_plane(omega, x0, R, npw) + plot(d, 1) + """ x0 = util.asarray_of_rows(x0) k = util.wavenumber(omega, c) @@ -274,6 +373,14 @@ def nfchoa_25d_point(omega, x0, r0, xs, max_order=None, c=None): See http://sfstoolbox.org/#equation-D.nfchoa.ps.2.5D. + Examples + -------- + .. plot:: + :context: close-figs + + d = sfs.mono.drivingfunction.nfchoa_25d_point(omega, x0, R, xs) + plot(d, 1) + """ x0 = util.asarray_of_rows(x0) k = util.wavenumber(omega, c) @@ -302,6 +409,14 @@ def nfchoa_25d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): See http://sfstoolbox.org/#equation-D.nfchoa.pw.2.5D. + Examples + -------- + .. plot:: + :context: close-figs + + d = sfs.mono.drivingfunction.nfchoa_25d_plane(omega, x0, R, npw) + plot(d, 1) + """ x0 = util.asarray_of_rows(x0) k = util.wavenumber(omega, c) From e0d80ffc7495b36e1353a8ed73eea1291613ee9c Mon Sep 17 00:00:00 2001 From: Hagen Wierstorf Date: Tue, 12 Feb 2019 22:25:58 +0100 Subject: [PATCH 015/101] DOC: math notations for mono sources --- doc/math-definitions.rst | 3 +++ sfs/mono/source.py | 51 +++++++++++++++++++--------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/doc/math-definitions.rst b/doc/math-definitions.rst index ee88d28..33c66b2 100644 --- a/doc/math-definitions.rst +++ b/doc/math-definitions.rst @@ -8,3 +8,6 @@ \newcommand{\scalarprod}[2]{\left\langle#1,#2\right\rangle} \renewcommand{\vec}[1]{\mathbf{#1}} \newcommand{\wc}{\frac{\omega}{c}} + \newcommand{\w}{\omega} + \newcommand{\x}{\vec{x}} + \newcommand{\n}{\vec{n}} diff --git a/sfs/mono/source.py b/sfs/mono/source.py index 67ce330..95ebc95 100644 --- a/sfs/mono/source.py +++ b/sfs/mono/source.py @@ -1,5 +1,7 @@ """Compute the sound field generated by a sound source. +.. include:: math-definitions.rst + .. plot:: :context: reset @@ -31,7 +33,7 @@ def point(omega, x0, n0, grid, c=None): - """Sound pressure of a point source. + r"""Sound pressure of a point source. Parameters ---------- @@ -54,11 +56,9 @@ def point(omega, x0, n0, grid, c=None): Notes ----- - :: + .. math:: - 1 e^(-j w/c |x-x0|) - G(x-x0, w) = --- ----------------- - 4pi |x-x0| + G(\x-\x_0,\w) = \frac{1}{4\pi} \frac{\e{-\i\wc|\x-\x_0|}}{|\x-\x_0|} Examples -------- @@ -188,11 +188,11 @@ def point_dipole(omega, x0, n0, grid, c=None): Notes ----- - :: + .. math:: - d 1 / iw 1 \ (x-x0) n0 - ---- G(x-x0,w) = --- | ----- + ------- | ----------- e^(-i w/c |x-x0|) - d ns 4pi \ c |x-x0| / |x-x0|^2 + G(\x-\x_0,\w) = \frac{1}{4\pi} \left(\i\wc + \frac{1}{|\x-\x_0|}\right) + \frac{\scalarprod{\x-\x_0}{\n_\text{s}}}{|\x-\x_0|^2} + \e{-\i\wc|\x-\x_0} Examples -------- @@ -406,16 +406,15 @@ def point_image_sources(omega, x0, n0, grid, L, max_order, coeffs=None, def line(omega, x0, n0, grid, c=None): - """Line source parallel to the z-axis. + r"""Line source parallel to the z-axis. Note: third component of x0 is ignored. Notes ----- - :: + .. math:: - (2) - G(x-x0, w) = -j/4 H0 (w/c |x-x0|) + G(\x-\x_0,\w) = -\frac{\i}{4} \Hankel{2}{0}{\wc|\x-\x_0|} Examples -------- @@ -486,17 +485,15 @@ def line_velocity(omega, x0, n0, grid, c=None): def line_dipole(omega, x0, n0, grid, c=None): - """Line source with dipole characteristics parallel to the z-axis. + r"""Line source with dipole characteristics parallel to the z-axis. Note: third component of x0 is ignored. Notes ----- - :: - - (2) - G(x-x0, w) = jk/4 H1 (w/c |x-x0|) cos(phi) + .. math:: + G(\x-\x_0,\w) = \frac{\i k}{4} \Hankel{2}{1}{\wc|\x-\x_0|} \cos{\phi} """ k = util.wavenumber(omega, c) @@ -577,7 +574,7 @@ def line_dirichlet_edge(omega, x0, grid, alpha=3/2*np.pi, Nc=None, c=None): def plane(omega, x0, n0, grid, c=None): - """Plane wave. + r"""Plane wave. Parameters ---------- @@ -600,9 +597,9 @@ def plane(omega, x0, n0, grid, c=None): Notes ----- - :: + .. math:: - G(x, w) = e^(-i w/c n x) + G(\x,\w) = \e{-\i\wc\n\x} Examples -------- @@ -624,7 +621,7 @@ def plane(omega, x0, n0, grid, c=None): def plane_velocity(omega, x0, n0, grid, c=None): - """Velocity of a plane wave. + r"""Velocity of a plane wave. Parameters ---------- @@ -647,9 +644,9 @@ def plane_velocity(omega, x0, n0, grid, c=None): Notes ----- - :: + .. math:: - V(x, w) = 1/(rho c) e^(-i w/c n x) n + V(\x,\w) = \frac{1}{\rho c} \e{-\i\wc\n\x} \n Examples -------- @@ -671,7 +668,7 @@ def plane_velocity(omega, x0, n0, grid, c=None): def plane_averaged_intensity(omega, x0, n0, grid, c=None): - """Averaged intensity of a plane wave. + r"""Averaged intensity of a plane wave. Parameters ---------- @@ -694,9 +691,9 @@ def plane_averaged_intensity(omega, x0, n0, grid, c=None): Notes ----- - :: + .. math:: - I(x, w) = 1/(2 rho c) n + I(\x,\w) = \frac{1}{2\rho c} \n """ if c is None: From 44e17c7430963ad7bd26dd0f9b86f350a3d90bee Mon Sep 17 00:00:00 2001 From: Hagen Wierstorf Date: Tue, 12 Feb 2019 22:52:14 +0100 Subject: [PATCH 016/101] DOC: math notations for mono driving functions --- sfs/mono/drivingfunction.py | 82 +++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/sfs/mono/drivingfunction.py b/sfs/mono/drivingfunction.py index 15f05d6..1ed077a 100644 --- a/sfs/mono/drivingfunction.py +++ b/sfs/mono/drivingfunction.py @@ -40,11 +40,13 @@ def plot(d, selected): def wfs_2d_line(omega, x0, n0, xs, c=None): - """Line source by 2-dimensional WFS. + r"""Line source by 2-dimensional WFS. - :: + .. math:: - D(x0,k) = j/2 k (x0-xs) n0 / |x0-xs| * H1(k |x0-xs|) + D(\x_0,\w) = \frac{\i}{2} \wc + \frac{\scalarprod{\x-\x_0}{\n_0}}{|\x-\x_\text{s}|} + \Hankel{2}{1}{\wc|\x-\x_\text{s}|} Examples -------- @@ -66,13 +68,13 @@ def wfs_2d_line(omega, x0, n0, xs, c=None): def _wfs_point(omega, x0, n0, xs, c=None): - """Point source by two- or three-dimensional WFS. + r"""Point source by two- or three-dimensional WFS. - :: + .. math:: - (x0-xs) n0 - D(x0,k) = j k ------------- e^(-j k |x0-xs|) - |x0-xs|^(3/2) + D(\x_0, \w) = \i\wc \frac{\scalarprod{\x_0-\x_\text{s}}{\n_0}} + {|\x_0-\x_\text{s}|^{\frac{3}{2}}} + \e{-\i\wc |\x_0-\x_\text{s}|} Examples -------- @@ -99,11 +101,12 @@ def _wfs_point(omega, x0, n0, xs, c=None): def wfs_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): r"""Point source by 2.5-dimensional WFS. - :: + .. math:: - ____________ (x0-xs) n0 - D(x0,k) = \|j k |xref-x0| ------------- e^(-j k |x0-xs|) - |x0-xs|^(3/2) + D(\x_0,\w) = \sqrt{\i\wc |\x_\text{ref}-\x_0|} + \frac{\scalarprod{\x_0-\x_\text{s}}{\n_0}} + {|\x_0-\x_\text{s}|^\frac{3}{2}} + \e{-\i\wc |\x_0-\x_\text{s}|} Examples -------- @@ -132,11 +135,14 @@ def wfs_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): def _wfs_plane(omega, x0, n0, n=[0, 1, 0], c=None): - """Plane wave by two- or three-dimensional WFS. + r"""Plane wave by two- or three-dimensional WFS. - Eq.(17) from :cite:`Spors2008`:: + Eq.(17) from :cite:`Spors2008`: - D(x0,k) = j k n n0 e^(-j k n x0) + .. math:: + + D(\x_0,\w) = \i\wc \scalarprod{\n}{\n_0} + \e{-\i\wc\scalarprod{\n}{\x_0}} Examples -------- @@ -162,10 +168,11 @@ def wfs_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, omalias=None): r"""Plane wave by 2.5-dimensional WFS. - :: + .. math:: - ____________ - D_2.5D(x0,w) = \|j k |xref-x0| n n0 e^(-j k n x0) + D_\text{2.5D}(\x_0,\w) = \sqrt{\i\wc |\x_\text{ref}-\x_0|} + \scalarprod{\n}{\n_0} + \e{-\i\wc \scalarprod{\n}{\x_0}} Examples -------- @@ -191,13 +198,13 @@ def wfs_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, def _wfs_focused(omega, x0, n0, xs, c=None): - """Focused source by two- or three-dimensional WFS. + r"""Focused source by two- or three-dimensional WFS. - :: + .. math:: - (x0-xs) n0 - D(x0,k) = j k ------------- e^(j k |x0-xs|) - |x0-xs|^(3/2) + D(\x_0,\w) = \i\wc \frac{\scalarprod{\x_0-\x_\text{s}}{\n_0}} + {|\x_0-\x_\text{s}|^\frac{3}{2}} + \e{\i\wc |\x_0-\x_\text{s}|} Examples -------- @@ -224,11 +231,12 @@ def _wfs_focused(omega, x0, n0, xs, c=None): def wfs_25d_focused(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): r"""Focused source by 2.5-dimensional WFS. - :: + .. math:: - ____________ (x0-xs) n0 - D(x0,w) = \|j k |xref-x0| ------------- e^(j k |x0-xs|) - |x0-xs|^(3/2) + D(\x_0,\w) = \sqrt{\i\wc |\x_\text{ref}-\x_0|} + \frac{\scalarprod{\x_0-\x_\text{s}}{\n_0}} + {|\x_0-\x_\text{s}|^\frac{3}{2}} + \e{\i\wc |\x_0-\x_\text{s}|} Examples -------- @@ -435,9 +443,7 @@ def sdm_2d_line(omega, x0, n0, xs, c=None): """Line source by two-dimensional SDM. The secondary sources have to be located on the x-axis (y0=0). - Derived from :cite:`Spors2009`, Eq.(9), Eq.(4):: - - D(x0,k) = + Derived from :cite:`Spors2009`, Eq.(9), Eq.(4). """ x0 = util.asarray_of_rows(x0) @@ -450,12 +456,14 @@ def sdm_2d_line(omega, x0, n0, xs, c=None): def sdm_2d_plane(omega, x0, n0, n=[0, 1, 0], c=None): - """Plane wave by two-dimensional SDM. + r"""Plane wave by two-dimensional SDM. The secondary sources have to be located on the x-axis (y0=0). - Derived from :cite:`Ahrens2012`, Eq.(3.73), Eq.(C.5), Eq.(C.11):: + Derived from :cite:`Ahrens2012`, Eq.(3.73), Eq.(C.5), Eq.(C.11): + + .. math:: - D(x0,k) = kpw,y * e^(-j*kpw,x*x) + D(\x_0,k) = k_\text{pw,y} \e{-\i k_\text{pw,x} x} """ x0 = util.asarray_of_rows(x0) @@ -469,9 +477,7 @@ def sdm_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): """Plane wave by 2.5-dimensional SDM. The secondary sources have to be located on the x-axis (y0=0). - Eq.(3.79) from :cite:`Ahrens2012`:: - - D_2.5D(x0,w) = + Eq.(3.79) from :cite:`Ahrens2012`. """ x0 = util.asarray_of_rows(x0) @@ -487,9 +493,7 @@ def sdm_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None): """Point source by 2.5-dimensional SDM. The secondary sources have to be located on the x-axis (y0=0). - Driving funcnction from :cite:`Spors2010`, Eq.(24):: - - D(x0,k) = + Driving funcnction from :cite:`Spors2010`, Eq.(24). """ x0 = util.asarray_of_rows(x0) From 94b79ce0b582843890f6c0924354d2993779b7db Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 19 Feb 2019 13:21:38 +0100 Subject: [PATCH 017/101] DOC: Use the "autosummary" extension --- doc/api.rst | 14 ++++++++++++++ doc/arrays.rst | 13 ------------- doc/conf.py | 3 +++ doc/frequency-domain.rst | 19 ------------------- doc/index.rst | 6 +----- doc/plotting.rst | 4 ---- doc/time-domain.rst | 16 ---------------- doc/utilities.rst | 4 ---- doc/version-history.rst | 2 ++ sfs/array.py | 3 --- sfs/mono/__init__.py | 12 +++++++++++- sfs/time/__init__.py | 12 +++++++++++- 12 files changed, 42 insertions(+), 66 deletions(-) create mode 100644 doc/api.rst delete mode 100644 doc/arrays.rst delete mode 100644 doc/frequency-domain.rst delete mode 100644 doc/plotting.rst delete mode 100644 doc/time-domain.rst delete mode 100644 doc/utilities.rst diff --git a/doc/api.rst b/doc/api.rst new file mode 100644 index 0000000..514be20 --- /dev/null +++ b/doc/api.rst @@ -0,0 +1,14 @@ +API Documentation +================= + +.. currentmodule:: sfs + +.. autosummary:: + :toctree: + + array + tapering + mono + time + plot + util diff --git a/doc/arrays.rst b/doc/arrays.rst deleted file mode 100644 index 611fb79..0000000 --- a/doc/arrays.rst +++ /dev/null @@ -1,13 +0,0 @@ -Secondary Sources -================= - -Loudspeaker Arrays ------------------- - -.. automodule:: sfs.array - :exclude-members: ArrayData - -Tapering --------- - -.. automodule:: sfs.tapering diff --git a/doc/conf.py b/doc/conf.py index 81003bf..59904e1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -32,6 +32,7 @@ # ones. extensions = [ 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', # support for NumPy-style docstrings @@ -49,6 +50,8 @@ autodoc_member_order = 'bysource' autodoc_default_flags = ['members', 'undoc-members'] +autosummary_generate = ['api'] + napoleon_google_docstring = False napoleon_numpy_docstring = True napoleon_include_private_with_doc = False diff --git a/doc/frequency-domain.rst b/doc/frequency-domain.rst deleted file mode 100644 index b741be7..0000000 --- a/doc/frequency-domain.rst +++ /dev/null @@ -1,19 +0,0 @@ -Frequency Domain -================ - -.. automodule:: sfs.mono - -Monochromatic Sources ---------------------- - -.. automodule:: sfs.mono.source - -Monochromatic Driving Functions -------------------------------- - -.. automodule:: sfs.mono.drivingfunction - -Monochromatic Sound Fields --------------------------- - -.. automodule:: sfs.mono.synthesized diff --git a/doc/index.rst b/doc/index.rst index fd5cea3..2c37758 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -6,11 +6,7 @@ installation examples/index - arrays - frequency-domain - time-domain - plotting - utilities + api references contributing version-history diff --git a/doc/plotting.rst b/doc/plotting.rst deleted file mode 100644 index 75ebcf5..0000000 --- a/doc/plotting.rst +++ /dev/null @@ -1,4 +0,0 @@ -Plotting -======== - -.. automodule:: sfs.plot diff --git a/doc/time-domain.rst b/doc/time-domain.rst deleted file mode 100644 index cd120a8..0000000 --- a/doc/time-domain.rst +++ /dev/null @@ -1,16 +0,0 @@ -Time Domain -=========== - -Time Domain Sources -------------------- - -.. automodule:: sfs.time.source - -Time Domain Driving Functions ------------------------------ - -.. automodule:: sfs.time.drivingfunction - -Time Domain Sound Fields ------------------------- -.. automodule:: sfs.time.soundfield diff --git a/doc/utilities.rst b/doc/utilities.rst deleted file mode 100644 index 861fdfd..0000000 --- a/doc/utilities.rst +++ /dev/null @@ -1,4 +0,0 @@ -Utilities -========= - -.. automodule:: sfs.util diff --git a/doc/version-history.rst b/doc/version-history.rst index 291074a..8f23a1f 100644 --- a/doc/version-history.rst +++ b/doc/version-history.rst @@ -1 +1,3 @@ +.. default-role:: py:obj + .. include:: ../NEWS.rst diff --git a/sfs/array.py b/sfs/array.py index 7697fa1..8b0fe80 100644 --- a/sfs/array.py +++ b/sfs/array.py @@ -8,9 +8,6 @@ plt.rcParams['figure.figsize'] = 8, 4.5 # inch plt.rcParams['axes.grid'] = True -.. autoclass:: ArrayData - :members: take - """ from collections import namedtuple import numpy as np diff --git a/sfs/mono/__init__.py b/sfs/mono/__init__.py index 8c09fd8..6f3ee6d 100644 --- a/sfs/mono/__init__.py +++ b/sfs/mono/__init__.py @@ -1,4 +1,14 @@ -"""Submodules for monochromatic sound fields.""" +"""Submodules for monochromatic sound fields. + +.. autosummary:: + :toctree: + + drivingfunction + source + synthesized + soundfigure + +""" from . import drivingfunction from . import source from . import synthesized diff --git a/sfs/time/__init__.py b/sfs/time/__init__.py index f4f826a..cd7430b 100644 --- a/sfs/time/__init__.py +++ b/sfs/time/__init__.py @@ -1,3 +1,13 @@ +"""Submodules for broadband sound fields. + +.. autosummary:: + :toctree: + + drivingfunction + source + soundfield + +""" from . import drivingfunction from . import source -from . import soundfield \ No newline at end of file +from . import soundfield From 94ee08eb47542de28014413f7f77c4a0bf48a5f4 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 21 Feb 2019 12:15:45 +0100 Subject: [PATCH 018/101] DOC: Remove make_movie.py example --- doc/examples/make_movie.py | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 doc/examples/make_movie.py diff --git a/doc/examples/make_movie.py b/doc/examples/make_movie.py deleted file mode 100644 index 8f4b82a..0000000 --- a/doc/examples/make_movie.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Example how to generate an animation from a pre-computed sound field. - -p and grid should contain the pressure field and axes of the sound -field, respectively. - -""" - -import numpy as np -import matplotlib.pyplot as plt -import sfs - -# total number of frames -frames = 240 - -fig = plt.figure(figsize=(15, 15)) -for i in range(frames): - plt.cla() - ph = sfs.mono.synthesized.shiftphase(p, i / frames * 4 * np.pi) - sfs.plot.soundfield(2.5e-9 * ph, grid, colorbar=False, cmap=plt.cm.BrBG) - fname = '_tmp%03d.png' % i - print('Saving frame', fname) - plt.savefig(fname) - - -# mencoder command line to convert PNGs to movie -# mencoder 'mf://_tmp*.png' -mf type=png:fps=30 -ovc lavc -lavcopts vcodec=wmv2 -oac copy -o soundfigure.mpg From 8449a769377326d4da5a941f7c9eae538771b645 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sun, 10 Feb 2019 21:01:29 +0100 Subject: [PATCH 019/101] TST: Make script to run all test scripts; use it on Travis-CI --- .travis.yml | 3 +++ doc/examples/run_all.py | 21 +++++++++++++++++++++ doc/examples/sound_field_synthesis.py | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100755 doc/examples/run_all.py diff --git a/.travis.yml b/.travis.yml index 05d7c9b..8324188 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,10 @@ install: - pip install . - pip install -r tests/requirements.txt - pip install -r doc/requirements.txt + # This is needed in example scripts: + - pip install pillow script: - python -m pytest + - python doc/examples/run_all.py # This executes the example notebooks: - python -m sphinx doc/ _build/ -b dummy diff --git a/doc/examples/run_all.py b/doc/examples/run_all.py new file mode 100755 index 0000000..1e913e9 --- /dev/null +++ b/doc/examples/run_all.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +from pathlib import Path +import subprocess +import sys + +if __name__ != '__main__': + raise ImportError(__name__ + ' is not meant be imported') + +self = Path(__file__) +cwd = self.parent + +for script in cwd.glob('*.py'): + if self == script: + # Don't call yourself! + continue + print('Running', script, '...') + args = [sys.executable, str(script.relative_to(cwd))] + sys.argv[1:] + result = subprocess.run(args, cwd=str(cwd)) + if result.returncode: + print('Error running', script, file=sys.stderr) + sys.exit(result.returncode) diff --git a/doc/examples/sound_field_synthesis.py b/doc/examples/sound_field_synthesis.py index a664889..2dfef55 100644 --- a/doc/examples/sound_field_synthesis.py +++ b/doc/examples/sound_field_synthesis.py @@ -33,7 +33,7 @@ #x0, n0, a0 = sfs.array.linear_random(N, 0.2*dx, 5*dx) #x0, n0, a0 = sfs.array.rectangular(N, dx, orientation=sfs.util.direction_vector(0*np.pi/4, np.pi/2)) #x0, n0, a0 = sfs.array.circular(N, R) -x0, n0, a0 = sfs.array.load('../data/arrays/university_rostock.csv') +x0, n0, a0 = sfs.array.load('../../data/arrays/university_rostock.csv') #x0, n0, a0 = sfs.array.planar(N, dx, orientation=sfs.util.direction_vector(np.radians(0),np.radians(180))) #x0, n0, a0 = sfs.array.cube(N, dx, orientation=sfs.util.direction_vector(0, np.pi/2)) From 7e099d88f7b1ebc2d9467c6303171e7110e30e84 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sun, 10 Feb 2019 22:18:49 +0100 Subject: [PATCH 020/101] DOC: SVG plots in HTML, PDF plots in LaTeX --- doc/conf.py | 8 +++++++- doc/examples/ipython_kernel_config.py | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 doc/examples/ipython_kernel_config.py diff --git a/doc/conf.py b/doc/conf.py index 59904e1..316053c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -46,6 +46,11 @@ # Override kernel name to allow running with Python 2 on Travis-CI nbsphinx_kernel_name = 'python' +nbsphinx_execute_arguments = [ + "--InlineBackend.figure_formats={'svg', 'pdf'}", + "--InlineBackend.rc={'figure.dpi': 96}", +] + autoclass_content = 'init' autodoc_member_order = 'bysource' autodoc_default_flags = ['members', 'undoc-members'] @@ -73,7 +78,8 @@ plot_include_source = True plot_html_show_source_link = False plot_html_show_formats = False -plot_pre_code = "" +plot_pre_code = '' +plot_formats = ['svg', 'pdf'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_template'] diff --git a/doc/examples/ipython_kernel_config.py b/doc/examples/ipython_kernel_config.py new file mode 100644 index 0000000..c1fbebf --- /dev/null +++ b/doc/examples/ipython_kernel_config.py @@ -0,0 +1,4 @@ +# See https://nbviewer.jupyter.org/github/mgeier/python-audio/blob/master/plotting/matplotlib-inline-defaults.ipynb + +c.InlineBackend.figure_formats = {'svg'} +c.InlineBackend.rc = {'figure.dpi': 96} From 95df172afc865286cb92a45151d4991e22e1ffc2 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sun, 10 Feb 2019 22:45:46 +0100 Subject: [PATCH 021/101] DOC: Update RTD settings --- doc/readthedocs-environment.yml | 2 +- readthedocs.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/readthedocs-environment.yml b/doc/readthedocs-environment.yml index 7d34622..d5ff057 100644 --- a/doc/readthedocs-environment.yml +++ b/doc/readthedocs-environment.yml @@ -1,7 +1,7 @@ channels: - conda-forge dependencies: - - python==3.5 + - python>=3 - sphinx>=1.3.6 - sphinx_rtd_theme - sphinxcontrib-bibtex diff --git a/readthedocs.yml b/readthedocs.yml index 03d0604..65bb0f4 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -1,4 +1,4 @@ conda: file: doc/readthedocs-environment.yml python: - setup_py_install: true + pip_install: true From 417278d96dbdf745781243ac4c5bddc1ba3ccf7d Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sun, 10 Feb 2019 22:49:15 +0100 Subject: [PATCH 022/101] Remove unnecessary Python 2 compatibility setting --- doc/conf.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 316053c..f60e370 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -43,9 +43,6 @@ 'nbsphinx', ] -# Override kernel name to allow running with Python 2 on Travis-CI -nbsphinx_kernel_name = 'python' - nbsphinx_execute_arguments = [ "--InlineBackend.figure_formats={'svg', 'pdf'}", "--InlineBackend.rc={'figure.dpi': 96}", From d13ff58ad383bd9147459158b95bab632163fd08 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sun, 10 Feb 2019 23:03:30 +0100 Subject: [PATCH 023/101] DOC: Some nbsphinx-related settings --- doc/conf.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index f60e370..e56bc54 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -155,6 +155,48 @@ # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False +jinja_define = """ +{% set docname = env.doc2path(env.docname, base='doc') %} +{% set latex_href = ''.join([ + '\href{https://github.com/sfstoolbox/sfs-python/blob/', + env.config.release, + '/', + docname | escape_latex, + '}{\sphinxcode{\sphinxupquote{', + docname | escape_latex, + '}}}', +]) %} +""" + +nbsphinx_prolog = jinja_define + r""" +.. only:: html + + .. role:: raw-html(raw) + :format: html + + .. nbinfo:: + + This page was generated from `{{ docname }}`__. + Interactive online version: + :raw-html:`Binder badge` + + __ https://github.com/sfstoolbox/sfs-python/blob/ + {{ env.config.release }}/{{ docname }} + +.. raw:: latex + + \nbsphinxstartnotebook{\scriptsize\noindent\strut + \textcolor{gray}{The following section was generated from {{ latex_href }} + \dotfill}} +""" + +nbsphinx_epilog = jinja_define + r""" +.. raw:: latex + + \nbsphinxstopnotebook{\scriptsize\noindent\strut + \textcolor{gray}{\dotfill\ {{ latex_href }} ends here.}} +""" + # -- Options for HTML output ---------------------------------------------- @@ -228,6 +270,7 @@ def setup(app): # If true, links to the reST sources are added to the pages. html_show_sourcelink = True +html_sourcelink_suffix = '' # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True @@ -246,20 +289,32 @@ def setup(app): # Output file base name for HTML help builder. htmlhelp_basename = 'SFS' +html_scaled_image_link = False # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -'papersize': 'a4paper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -'printindex': '', + 'papersize': 'a4paper', + 'printindex': '', + 'sphinxsetup': r""" + VerbatimColor={HTML}{F5F5F5}, + VerbatimBorderColor={HTML}{E0E0E0}, + noteBorderColor={HTML}{E0E0E0}, + noteborder=1.5pt, + warningBorderColor={HTML}{E0E0E0}, + warningborder=1.5pt, + warningBgColor={HTML}{FBFBFB}, + """, + 'preamble': r""" +\usepackage[sc,osf]{mathpazo} +\linespread{1.05} % see http://www.tug.dk/FontCatalogue/urwpalladio/ +\renewcommand{\sfdefault}{pplj} % Palatino instead of sans serif +\IfFileExists{zlmtt.sty}{ + \usepackage[light,scaled=1.05]{zlmtt} % light typewriter font from lmodern +}{ + \renewcommand{\ttdefault}{lmtt} % typewriter font from lmodern +} +""", } # Grouping the document tree into LaTeX files. List of tuples From b9815f1b0bef694bf5caaeaf7341cd93052459c5 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 26 Feb 2019 11:06:08 +0100 Subject: [PATCH 024/101] TST: Don't try to call ipython_kernel_config.py as test script --- doc/examples/run_all.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/examples/run_all.py b/doc/examples/run_all.py index 1e913e9..c446af7 100755 --- a/doc/examples/run_all.py +++ b/doc/examples/run_all.py @@ -13,6 +13,9 @@ if self == script: # Don't call yourself! continue + if script.name == 'ipython_kernel_config.py': + # This is a configuration file, not an example script + continue print('Running', script, '...') args = [sys.executable, str(script.relative_to(cwd))] + sys.argv[1:] result = subprocess.run(args, cwd=str(cwd)) From f675e3651979c9dd07a530a2e88ce65f5306150e Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 11 Feb 2019 16:58:09 +0100 Subject: [PATCH 025/101] Use sfs.default object instead of sfs.defs submodule Closes #32. --- doc/api.rst | 1 + doc/conf.py | 5 +++++ sfs/__init__.py | 39 ++++++++++++++++++++++++++++++++++++- sfs/defs.py | 10 ---------- sfs/mono/drivingfunction.py | 8 ++++---- sfs/mono/source.py | 22 ++++++++++----------- sfs/plot.py | 4 ++-- sfs/time/drivingfunction.py | 8 ++++---- sfs/time/soundfield.py | 4 ++-- sfs/time/source.py | 4 ++-- sfs/util.py | 4 ++-- 11 files changed, 71 insertions(+), 38 deletions(-) delete mode 100644 sfs/defs.py diff --git a/doc/api.rst b/doc/api.rst index 514be20..b556288 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -12,3 +12,4 @@ API Documentation time plot util + default diff --git a/doc/conf.py b/doc/conf.py index e56bc54..cb64c63 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -17,6 +17,8 @@ import os from subprocess import check_output +import sphinx + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -48,6 +50,9 @@ "--InlineBackend.rc={'figure.dpi': 96}", ] +# Tell autodoc that the documentation is being generated +sphinx.SFS_DOCS_ARE_BEING_BUILT = True + autoclass_content = 'init' autodoc_member_order = 'bysource' autodoc_default_flags = ['members', 'undoc-members'] diff --git a/sfs/__init__.py b/sfs/__init__.py index e1578b4..0a24570 100644 --- a/sfs/__init__.py +++ b/sfs/__init__.py @@ -5,10 +5,47 @@ """ __version__ = "0.4.0" + +class default: + """Get/set defaults for the *sfs* module. + + For example, when you want to change the default speed of sound:: + + import sfs + sfs.default.c = 330 + + """ + + c = 343 + """Speed of sound.""" + + rho0 = 1.2250 + """Static density of air.""" + + selection_tolerance = 1e-6 + """Tolerance used for secondary source selection.""" + + def __setattr__(self, name, value): + """Only allow setting existing attributes.""" + if name in dir(self) and name != 'reset': + super().__setattr__(name, value) + else: + raise AttributeError( + '"default" object has no attribute ' + repr(name)) + + def reset(self): + """Reset all attributes to their "factory default".""" + vars(self).clear() + + +import sys as _sys +if not getattr(_sys.modules.get('sphinx'), 'SFS_DOCS_ARE_BEING_BUILT', False): + # This object shadows the 'default' class, except when the docs are built: + default = default() + from . import tapering from . import array from . import util -from . import defs try: from . import plot except ImportError: diff --git a/sfs/defs.py b/sfs/defs.py deleted file mode 100644 index c7a36ad..0000000 --- a/sfs/defs.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Definition of constants.""" - -# speed of sound -c = 343 - -# static density of air -rho0 = 1.2250 - -# tolerance used for secondary source selection -selection_tolerance = 1e-6 diff --git a/sfs/mono/drivingfunction.py b/sfs/mono/drivingfunction.py index 1ed077a..acca3ab 100644 --- a/sfs/mono/drivingfunction.py +++ b/sfs/mono/drivingfunction.py @@ -36,7 +36,7 @@ def plot(d, selected): from numpy.core.umath_tests import inner1d # element-wise inner product from scipy.special import jn, hankel2 from .. import util -from .. import defs +from .. import default def wfs_2d_line(omega, x0, n0, xs, c=None): @@ -291,7 +291,7 @@ def source_selection_plane(n0, n): """ n0 = util.asarray_of_rows(n0) n = util.normalize_vector(n) - return np.inner(n, n0) >= defs.selection_tolerance + return np.inner(n, n0) >= default.selection_tolerance def source_selection_point(n0, x0, xs): @@ -304,7 +304,7 @@ def source_selection_point(n0, x0, xs): x0 = util.asarray_of_rows(x0) xs = util.asarray_1d(xs) ds = x0 - xs - return inner1d(ds, n0) >= defs.selection_tolerance + return inner1d(ds, n0) >= default.selection_tolerance def source_selection_line(n0, x0, xs): @@ -326,7 +326,7 @@ def source_selection_focused(ns, x0, xs): xs = util.asarray_1d(xs) ns = util.normalize_vector(ns) ds = xs - x0 - return inner1d(ns, ds) >= defs.selection_tolerance + return inner1d(ns, ds) >= default.selection_tolerance def source_selection_all(N): diff --git a/sfs/mono/source.py b/sfs/mono/source.py index 95ebc95..7bea0eb 100644 --- a/sfs/mono/source.py +++ b/sfs/mono/source.py @@ -16,7 +16,7 @@ normalization_point = 4 * np.pi normalization_line = \\ - np.sqrt(8 * np.pi * omega / sfs.defs.c) * np.exp(1j * np.pi / 4) + np.sqrt(8 * np.pi * omega / sfs.default.c) * np.exp(1j * np.pi / 4) grid = sfs.util.xyz_grid([-2, 3], [-1, 2], 0, spacing=0.02) @@ -29,7 +29,7 @@ import numpy as np from scipy import special from .. import util -from .. import defs +from .. import default def point(omega, x0, n0, grid, c=None): @@ -123,14 +123,14 @@ def point_velocity(omega, x0, n0, grid, c=None): """ if c is None: - c = defs.c + c = default.c k = util.wavenumber(omega, c) x0 = util.asarray_1d(x0) grid = util.as_xyz_components(grid) offset = grid - x0 r = np.linalg.norm(offset) v = point(omega, x0, n0, grid, c=c) - v *= (1+1j*k*r) / (defs.rho0 * c * 1j*k*r) + v *= (1+1j*k*r) / (default.rho0 * c * 1j*k*r) return util.XyzComponents([v * o / r for o in offset]) @@ -160,7 +160,7 @@ def point_averaged_intensity(omega, x0, n0, grid, c=None): grid = util.as_xyz_components(grid) offset = grid - x0 r = np.linalg.norm(offset) - i = 1 / (2*defs.rho0 * defs.c) + i = 1 / (2 * default.rho0 * default.c) return util.XyzComponents([i * o / r**2 for o in offset]) @@ -466,14 +466,14 @@ def line_velocity(omega, x0, n0, grid, c=None): """ if c is None: - c = defs.c + c = default.c k = util.wavenumber(omega, c) x0 = util.asarray_1d(x0)[:2] # ignore z-component grid = util.as_xyz_components(grid) offset = grid[:2] - x0 r = np.linalg.norm(offset) - v = -1/(4*c*defs.rho0) * special.hankel2(1, k * r) + v = -1/(4*c*default.rho0) * special.hankel2(1, k * r) v = [v * o / r for o in offset] assert v[0].shape == v[1].shape @@ -662,8 +662,8 @@ def plane_velocity(omega, x0, n0, grid, c=None): """ if c is None: - c = defs.c - v = plane(omega, x0, n0, grid, c=c) / (defs.rho0 * c) + c = default.c + v = plane(omega, x0, n0, grid, c=c) / (default.rho0 * c) return util.XyzComponents([v * n for n in n0]) @@ -697,8 +697,8 @@ def plane_averaged_intensity(omega, x0, n0, grid, c=None): """ if c is None: - c = defs.c - i = 1 / (2 * defs.rho0 * c) + c = default.c + i = 1 / (2 * default.rho0 * c) return util.XyzComponents([i * n for n in n0]) diff --git a/sfs/plot.py b/sfs/plot.py index b8411c2..495a570 100644 --- a/sfs/plot.py +++ b/sfs/plot.py @@ -8,7 +8,7 @@ from mpl_toolkits.mplot3d import Axes3D import numpy as np from . import util -from . import defs +from . import default def _register_cmap_clip(name, original_cmap, alpha): @@ -385,7 +385,7 @@ def vectors(v, grid, cmap='blacktransparent', headlength=3, headaxislength=2.5, if ax is None: ax = plt.gca() if clim is None: - v_ref = 1 / (defs.rho0 * defs.c) # reference particle velocity + v_ref = 1 / (default.rho0 * default.c) # reference particle velocity clim = 0, 2 * v_ref return ax.quiver(X, Y, U, V, speed, cmap=cmap, pivot='mid', units='xy', angles='xy', headlength=headlength, diff --git a/sfs/time/drivingfunction.py b/sfs/time/drivingfunction.py index a4e5c28..a6d2ae8 100644 --- a/sfs/time/drivingfunction.py +++ b/sfs/time/drivingfunction.py @@ -5,7 +5,7 @@ """ import numpy as np from numpy.core.umath_tests import inner1d # element-wise inner product -from .. import defs +from .. import default from .. import util @@ -56,7 +56,7 @@ def wfs_25d_plane(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): """ if c is None: - c = defs.c + c = default.c x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) n = util.normalize_vector(n) @@ -116,7 +116,7 @@ def wfs_25d_point(x0, n0, xs, xref=[0, 0, 0], c=None): """ if c is None: - c = defs.c + c = default.c x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) xs = util.asarray_1d(xs) @@ -179,7 +179,7 @@ def wfs_25d_focused(x0, n0, xs, xref=[0, 0, 0], c=None): """ if c is None: - c = defs.c + c = default.c x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) xs = util.asarray_1d(xs) diff --git a/sfs/time/soundfield.py b/sfs/time/soundfield.py index d81888e..7bbb05d 100644 --- a/sfs/time/soundfield.py +++ b/sfs/time/soundfield.py @@ -1,7 +1,7 @@ """Compute sound field.""" from .. import util -from .. import defs +from .. import default from .source import point @@ -38,7 +38,7 @@ def p_array(x0, signals, weights, observation_time, grid, source=point, """ if c is None: - c = defs.c + c = default.c x0 = util.asarray_of_rows(x0) data, samplerate, signal_offset = util.as_delayed_signal(signals) weights = util.asarray_1d(weights) diff --git a/sfs/time/source.py b/sfs/time/source.py index 994e15f..e9dab06 100644 --- a/sfs/time/source.py +++ b/sfs/time/source.py @@ -8,7 +8,7 @@ import numpy as np from .. import util -from .. import defs +from .. import default def point(xs, signal, observation_time, grid, c=None): @@ -51,7 +51,7 @@ def point(xs, signal, observation_time, grid, c=None): data = util.asarray_1d(data) grid = util.as_xyz_components(grid) if c is None: - c = defs.c + c = default.c r = np.linalg.norm(grid - xs) # evaluate g over grid weights = 1 / (4 * np.pi * r) diff --git a/sfs/util.py b/sfs/util.py index f91c4a6..8410675 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -7,7 +7,7 @@ import collections import numpy as np from scipy.special import spherical_jn, spherical_yn -from . import defs +from . import default def rotation_matrix(n1, n2): @@ -46,7 +46,7 @@ def rotation_matrix(n1, n2): def wavenumber(omega, c=None): """Compute the wavenumber for a given radial frequency.""" if c is None: - c = defs.c + c = default.c return omega / c From a4c7c8c2d35f130bb4768e63af20f99df509d166 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 26 Feb 2019 12:29:49 +0100 Subject: [PATCH 026/101] DOC: Move top-level "autosummary" to sfs module docstring --- doc/api.rst | 13 +------------ sfs/__init__.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index b556288..1e66983 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -1,15 +1,4 @@ API Documentation ================= -.. currentmodule:: sfs - -.. autosummary:: - :toctree: - - array - tapering - mono - time - plot - util - default +.. automodule:: sfs diff --git a/sfs/__init__.py b/sfs/__init__.py index 0a24570..eaae8e9 100644 --- a/sfs/__init__.py +++ b/sfs/__init__.py @@ -1,6 +1,18 @@ """Sound Field Synthesis Toolbox. -http://sfs.rtfd.org/ +http://python.sfstoolbox.org/ + +.. rubric:: Submodules + +.. autosummary:: + :toctree: + + array + tapering + mono + time + plot + util """ __version__ = "0.4.0" From 301c2d67704647b0ebb482052672197f269c5082 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 26 Feb 2019 13:03:07 +0100 Subject: [PATCH 027/101] DOC: Restructure "examples" page * Add symbolic link `examples/` that points to `doc/examples` * Move `.rst` files out of examples directory * Short explanation of Binder links * Python example scripts get their own sub-page (instead of cluttering the main example page) --- doc/conf.py | 5 ----- doc/example-python-scripts.rst | 11 +++++++++++ doc/examples.rst | 18 ++++++++++++++++++ doc/examples/index.rst | 23 ----------------------- doc/examples/ipython_kernel_config.py | 2 ++ doc/index.rst | 2 +- examples | 1 + 7 files changed, 33 insertions(+), 29 deletions(-) create mode 100644 doc/example-python-scripts.rst create mode 100644 doc/examples.rst delete mode 100644 doc/examples/index.rst create mode 120000 examples diff --git a/doc/conf.py b/doc/conf.py index cb64c63..a542b27 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -39,7 +39,6 @@ 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', # support for NumPy-style docstrings 'sphinx.ext.intersphinx', - 'sphinx.ext.extlinks', 'sphinxcontrib.bibtex', 'matplotlib.sphinxext.plot_directive', 'nbsphinx', @@ -113,10 +112,6 @@ except Exception: release = '' -binder_base_url = 'https://mybinder.org/v2/gh/sfstoolbox/sfs-python/' - -extlinks = {'binder': (binder_base_url + release + '?filepath=%s', 'binder:')} - # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None diff --git a/doc/example-python-scripts.rst b/doc/example-python-scripts.rst new file mode 100644 index 0000000..650e884 --- /dev/null +++ b/doc/example-python-scripts.rst @@ -0,0 +1,11 @@ +Example Python Scripts +====================== + +Various example scripts are located in the directory ``doc/examples/``, e.g. + +* :download:`examples/sound_field_synthesis.py`: Illustrates the general usage + of the toolbox +* :download:`examples/horizontal_plane_arrays.py`: Computes the sound fields + for various techniques, virtual sources and loudspeaker array configurations +* :download:`examples/soundfigures.py`: Illustrates the synthesis of sound + figures with Wave Field Synthesis diff --git a/doc/examples.rst b/doc/examples.rst new file mode 100644 index 0000000..be11b0d --- /dev/null +++ b/doc/examples.rst @@ -0,0 +1,18 @@ +Examples +======== + +.. only:: html + + You can play with the Jupyter notebooks (without having to install anything) + by clicking |binder logo| on the respective example page. + + .. |binder logo| image:: https://mybinder.org/badge_logo.svg + :target: https://mybinder.org/v2/gh/sfstoolbox/sfs-python/master? + filepath=doc/examples + +.. toctree:: + :maxdepth: 1 + + examples/modal-room-acoustics + examples/mirror-image-source-model + example-python-scripts diff --git a/doc/examples/index.rst b/doc/examples/index.rst deleted file mode 100644 index c737441..0000000 --- a/doc/examples/index.rst +++ /dev/null @@ -1,23 +0,0 @@ -Examples -======== - -Various examples are located in the directory ``doc/examples/`` as Python -scripts, e.g. - -* sound_field_synthesis.py: - Illustrates the general usage of the toolbox -* horizontal_plane_arrays.py: - Computes the sound fields for various techniques, virtual sources and loudspeaker array configurations -* soundfigures.py: - Illustrates the synthesis of sound figures with Wave Field Synthesis - - -Or Jupyter notebooks, which are also available online as interactive examples: -:binder:`doc/examples`. - - -.. toctree:: - :maxdepth: 1 - - modal-room-acoustics - mirror-image-source-model diff --git a/doc/examples/ipython_kernel_config.py b/doc/examples/ipython_kernel_config.py index c1fbebf..4cbbcc1 100644 --- a/doc/examples/ipython_kernel_config.py +++ b/doc/examples/ipython_kernel_config.py @@ -1,3 +1,5 @@ +# This is a configuration file that's used when opening the Jupyter notebooks +# in this directory. # See https://nbviewer.jupyter.org/github/mgeier/python-audio/blob/master/plotting/matplotlib-inline-defaults.ipynb c.InlineBackend.figure_formats = {'svg'} diff --git a/doc/index.rst b/doc/index.rst index 2c37758..e56a16f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -5,7 +5,7 @@ .. toctree:: installation - examples/index + examples api references contributing diff --git a/examples b/examples new file mode 120000 index 0000000..c75d4df --- /dev/null +++ b/examples @@ -0,0 +1 @@ +doc/examples/ \ No newline at end of file From e96f3346cf5a33d30c49d5167a0d3397fde39aa8 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 27 Feb 2019 16:13:27 +0100 Subject: [PATCH 028/101] Expose rho0 as function parameter Closes #70. --- sfs/mono/source.py | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/sfs/mono/source.py b/sfs/mono/source.py index 7bea0eb..75eaa8b 100644 --- a/sfs/mono/source.py +++ b/sfs/mono/source.py @@ -87,7 +87,7 @@ def point(omega, x0, n0, grid, c=None): return 1 / (4*np.pi) * np.exp(-1j * k * r) / r -def point_velocity(omega, x0, n0, grid, c=None): +def point_velocity(omega, x0, n0, grid, c=None, rho0=None): """Particle velocity of a point source. Parameters @@ -103,6 +103,8 @@ def point_velocity(omega, x0, n0, grid, c=None): See `sfs.util.xyz_grid()`. c : float, optional Speed of sound. + rho0 : float, optional + Static density of air. Returns ------- @@ -124,17 +126,19 @@ def point_velocity(omega, x0, n0, grid, c=None): """ if c is None: c = default.c + if rho0 is None: + rho0 = default.rho0 k = util.wavenumber(omega, c) x0 = util.asarray_1d(x0) grid = util.as_xyz_components(grid) offset = grid - x0 r = np.linalg.norm(offset) v = point(omega, x0, n0, grid, c=c) - v *= (1+1j*k*r) / (default.rho0 * c * 1j*k*r) + v *= (1+1j*k*r) / (rho0 * c * 1j*k*r) return util.XyzComponents([v * o / r for o in offset]) -def point_averaged_intensity(omega, x0, n0, grid, c=None): +def point_averaged_intensity(omega, x0, n0, grid, c=None, rho0=None): """Velocity of a point source. Parameters @@ -150,17 +154,23 @@ def point_averaged_intensity(omega, x0, n0, grid, c=None): See `sfs.util.xyz_grid()`. c : float, optional Speed of sound. + rho0 : float, optional + Static density of air. Returns ------- `XyzComponents` Averaged intensity at positions given by *grid*. """ + if c is None: + c = default.c + if rho0 is None: + rho0 = default.rho0 x0 = util.asarray_1d(x0) grid = util.as_xyz_components(grid) offset = grid - x0 r = np.linalg.norm(offset) - i = 1 / (2 * default.rho0 * default.c) + i = 1 / (2 * rho0 * c) return util.XyzComponents([i * o / r**2 for o in offset]) @@ -444,7 +454,7 @@ def line(omega, x0, n0, grid, c=None): return _duplicate_zdirection(p, grid) -def line_velocity(omega, x0, n0, grid, c=None): +def line_velocity(omega, x0, n0, grid, c=None, rho0=None): """Velocity of line source parallel to the z-axis. Returns @@ -467,13 +477,15 @@ def line_velocity(omega, x0, n0, grid, c=None): """ if c is None: c = default.c + if rho0 is None: + rho0 = default.rho0 k = util.wavenumber(omega, c) x0 = util.asarray_1d(x0)[:2] # ignore z-component grid = util.as_xyz_components(grid) offset = grid[:2] - x0 r = np.linalg.norm(offset) - v = -1/(4*c*default.rho0) * special.hankel2(1, k * r) + v = -1/(4 * c * rho0) * special.hankel2(1, k * r) v = [v * o / r for o in offset] assert v[0].shape == v[1].shape @@ -620,7 +632,7 @@ def plane(omega, x0, n0, grid, c=None): return np.exp(-1j * k * np.inner(grid - x0, n0)) -def plane_velocity(omega, x0, n0, grid, c=None): +def plane_velocity(omega, x0, n0, grid, c=None, rho0=None): r"""Velocity of a plane wave. Parameters @@ -636,6 +648,8 @@ def plane_velocity(omega, x0, n0, grid, c=None): See `sfs.util.xyz_grid()`. c : float, optional Speed of sound. + rho0 : float, optional + Static density of air. Returns ------- @@ -663,11 +677,13 @@ def plane_velocity(omega, x0, n0, grid, c=None): """ if c is None: c = default.c - v = plane(omega, x0, n0, grid, c=c) / (default.rho0 * c) + if rho0 is None: + rho0 = default.rho0 + v = plane(omega, x0, n0, grid, c=c) / (rho0 * c) return util.XyzComponents([v * n for n in n0]) -def plane_averaged_intensity(omega, x0, n0, grid, c=None): +def plane_averaged_intensity(omega, x0, n0, grid, c=None, rho0=None): r"""Averaged intensity of a plane wave. Parameters @@ -683,6 +699,8 @@ def plane_averaged_intensity(omega, x0, n0, grid, c=None): See `sfs.util.xyz_grid()`. c : float, optional Speed of sound. + rho0 : float, optional + Static density of air. Returns ------- @@ -698,7 +716,9 @@ def plane_averaged_intensity(omega, x0, n0, grid, c=None): """ if c is None: c = default.c - i = 1 / (2 * default.rho0 * c) + if rho0 is None: + rho0 = default.rho0 + i = 1 / (2 * rho0 * c) return util.XyzComponents([i * n for n in n0]) From 1e8f43f52ebc98f0f13878fdcced994e87650246 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sun, 10 Feb 2019 22:17:06 +0100 Subject: [PATCH 029/101] DOC: Make LaTeX math definitions LaTeX compatible --- doc/conf.py | 6 ++++++ doc/math-definitions.rst | 31 ++++++++++++++++++++----------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index a542b27..bd4d959 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -82,6 +82,12 @@ plot_pre_code = '' plot_formats = ['svg', 'pdf'] +mathjax_config = { + 'TeX': { + 'extensions': ['newcommand.js', 'begingroup.js'], # Support for \gdef + }, +} + # Add any paths that contain templates here, relative to this directory. templates_path = ['_template'] diff --git a/doc/math-definitions.rst b/doc/math-definitions.rst index 33c66b2..f5bba74 100644 --- a/doc/math-definitions.rst +++ b/doc/math-definitions.rst @@ -1,13 +1,22 @@ +.. raw:: latex + + \marginpar{% Avoid creating empty vertical space for the math definitions + .. rst-class:: hidden .. math:: - \newcommand{\dirac}[1]{\operatorname{\delta}\left(#1\right)} - \newcommand{\e}[1]{\operatorname{e}^{#1}} - \newcommand{\Hankel}[3]{\mathop{{}H_{#2}^{(#1)}}\!\left(#3\right)} - \newcommand{\hankel}[3]{\mathop{{}h_{#2}^{(#1)}}\!\left(#3\right)} - \newcommand{\i}{\mathrm{i}} - \newcommand{\scalarprod}[2]{\left\langle#1,#2\right\rangle} - \renewcommand{\vec}[1]{\mathbf{#1}} - \newcommand{\wc}{\frac{\omega}{c}} - \newcommand{\w}{\omega} - \newcommand{\x}{\vec{x}} - \newcommand{\n}{\vec{n}} + + \gdef\dirac#1{\operatorname{\delta}\left(#1\right)} + \gdef\e#1{\operatorname{e}^{#1}} + \gdef\Hankel#1#2#3{\mathop{{}H_{#2}^{(#1)}}\!\left(#3\right)} + \gdef\hankel#1#2#3{\mathop{{}h_{#2}^{(#1)}}\!\left(#3\right)} + \gdef\i{\mathrm{i}} + \gdef\scalarprod#1#2{\left\langle#1,#2\right\rangle} + \gdef\vec#1{\mathbf{#1}} + \gdef\wc{\frac{\omega}{c}} + \gdef\w{\omega} + \gdef\x{\vec{x}} + \gdef\n{\vec{n}} + +.. raw:: latex + + } From 00e96a9435f279b564b352e3a5c9f5e94da16853 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 27 Feb 2019 13:33:00 +0100 Subject: [PATCH 030/101] A few fixes suggested by pycodestyle --- sfs/mono/drivingfunction.py | 12 ++++++++---- sfs/plot.py | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/sfs/mono/drivingfunction.py b/sfs/mono/drivingfunction.py index acca3ab..001c579 100644 --- a/sfs/mono/drivingfunction.py +++ b/sfs/mono/drivingfunction.py @@ -507,7 +507,8 @@ def sdm_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None): xs[1] / r * hankel2(1, k * r) -def esa_edge_2d_plane(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, c=None): +def esa_edge_2d_plane(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, + c=None): """Plane wave by two-dimensional ESA for an edge-shaped secondary source distribution consisting of monopole line sources. @@ -565,7 +566,8 @@ def esa_edge_2d_plane(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, c=None): return 4*np.pi/alpha * d -def esa_edge_dipole_2d_plane(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, c=None): +def esa_edge_dipole_2d_plane(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, + c=None): """Plane wave by two-dimensional ESA for an edge-shaped secondary source distribution consisting of dipole line sources. @@ -683,7 +685,8 @@ def esa_edge_2d_line(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): return -1j*np.pi/alpha * d -def esa_edge_25d_point(omega, x0, xs, xref=[2, -2, 0], alpha=3/2*np.pi, Nc=None, c=None): +def esa_edge_25d_point(omega, x0, xs, xref=[2, -2, 0], alpha=3/2*np.pi, + Nc=None, c=None): """Point source by 2.5-dimensional ESA for an edge-shaped secondary source distribution constisting of monopole line sources. @@ -725,7 +728,8 @@ def esa_edge_25d_point(omega, x0, xs, xref=[2, -2, 0], alpha=3/2*np.pi, Nc=None, else: a = np.linalg.norm(xref-x0, axis=1)/np.linalg.norm(xref-xs) - return 1j*np.sqrt(a) * esa_edge_2d_line(omega, x0, xs, alpha=alpha, Nc=Nc, c=c) + return 1j*np.sqrt(a) * esa_edge_2d_line(omega, x0, xs, alpha=alpha, Nc=Nc, + c=c) def esa_edge_dipole_2d_line(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): diff --git a/sfs/plot.py b/sfs/plot.py index 495a570..adb4d1f 100644 --- a/sfs/plot.py +++ b/sfs/plot.py @@ -20,6 +20,7 @@ def _register_cmap_clip(name, original_cmap, alpha): cmap.set_under([alpha * c + 1 - alpha for c in cmap(0.0)[:3]]) plt.cm.register_cmap(cmap=cmap) + # The 'coolwarm' colormap is based on the paper # "Diverging Color Maps for Scientific Visualization" by Kenneth Moreland # http://www.sandia.gov/~kmorel/documents/ColorMaps/ @@ -37,6 +38,7 @@ def _register_cmap_transparent(name, color): cmap = LinearSegmentedColormap(name, cdict) plt.cm.register_cmap(cmap=cmap) + _register_cmap_transparent('blacktransparent', 'black') From 427e0529e790a65cea47e0470f8c339ca19a58c8 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 27 Feb 2019 13:45:22 +0100 Subject: [PATCH 031/101] A few fixes suggested by pydocstyle --- sfs/mono/soundfigure.py | 1 - sfs/mono/source.py | 5 +++-- sfs/plot.py | 2 +- sfs/util.py | 10 ++++++---- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/sfs/mono/soundfigure.py b/sfs/mono/soundfigure.py index 957b830..272bc4f 100644 --- a/sfs/mono/soundfigure.py +++ b/sfs/mono/soundfigure.py @@ -12,7 +12,6 @@ def wfs_3d_pw(omega, x0, n0, figure, npw=[0, 0, 1], c=None): [Helwani et al., The Synthesis of Sound Figures, MSSP, 2013] """ - x0 = np.asarray(x0) n0 = np.asarray(n0) k = util.wavenumber(omega, c) diff --git a/sfs/mono/source.py b/sfs/mono/source.py index 75eaa8b..7fc408d 100644 --- a/sfs/mono/source.py +++ b/sfs/mono/source.py @@ -1,4 +1,4 @@ -"""Compute the sound field generated by a sound source. +r"""Compute the sound field generated by a sound source. .. include:: math-definitions.rst @@ -15,7 +15,7 @@ omega = 2 * np.pi * f normalization_point = 4 * np.pi - normalization_line = \\ + normalization_line = \ np.sqrt(8 * np.pi * omega / sfs.default.c) * np.exp(1j * np.pi / 4) grid = sfs.util.xyz_grid([-2, 3], [-1, 2], 0, spacing=0.02) @@ -161,6 +161,7 @@ def point_averaged_intensity(omega, x0, n0, grid, c=None, rho0=None): ------- `XyzComponents` Averaged intensity at positions given by *grid*. + """ if c is None: c = default.c diff --git a/sfs/plot.py b/sfs/plot.py index adb4d1f..4457848 100644 --- a/sfs/plot.py +++ b/sfs/plot.py @@ -326,7 +326,7 @@ def level(p, grid, xnorm=None, power=False, cmap=None, vmax=3, vmin=-50, def particles(x, trim=None, ax=None, xlabel='x (m)', ylabel='y (m)', edgecolor='', **kwargs): - """Plot particle positions as scatter plot""" + """Plot particle positions as scatter plot.""" XX, YY = [np.real(c) for c in x[:2]] if trim is not None: diff --git a/sfs/util.py b/sfs/util.py index 8410675..7c75ff8 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -83,6 +83,7 @@ def sph2cart(alpha, beta, r): y-component of Cartesian coordinates z : float or array_like z-component of Cartesian coordinates + """ x = r * np.cos(alpha) * np.sin(beta) y = r * np.sin(alpha) * np.sin(beta) @@ -119,6 +120,7 @@ def cart2sph(x, y, z): Elevation angle in radiants (with 0 denoting North pole) r : float or array_like Radius + """ r = np.sqrt(x**2 + y**2 + z**2) alpha = np.arctan2(y, x) @@ -158,7 +160,7 @@ def asarray_of_rows(a, **kwargs): def as_xyz_components(components, **kwargs): - """Convert *components* to `XyzComponents` of `numpy.ndarray`\\s. + r"""Convert *components* to `XyzComponents` of `numpy.ndarray`\s. The *components* are first converted to NumPy arrays (using :func:`numpy.asarray`) which are then assembled into an @@ -338,7 +340,7 @@ def normalize_vector(x): def displacement(v, omega): - r"""Particle displacement + r"""Particle displacement. .. math:: @@ -368,7 +370,7 @@ class XyzComponents(np.ndarray): """See __init__().""" def __init__(self, components): - """Triple (or pair) of components: x, y, and optionally z. + r"""Triple (or pair) of components: x, y, and optionally z. Instances of this class can be used to store coordinate grids (either regular grids like in `xyz_grid()` or arbitrary point @@ -377,7 +379,7 @@ def __init__(self, components): This class is a subclass of `numpy.ndarray`. It is one-dimensional (like a plain `list`) and has a length of 3 (or 2, if no z-component is available). It uses ``dtype=object`` in - order to be able to store other `numpy.ndarray`\\s of arbitrary + order to be able to store other `numpy.ndarray`\s of arbitrary shapes but also scalars, if needed. Because it is a NumPy array subclass, it can be used in operations with scalars and "normal" NumPy arrays, as long as they have a compatible shape. Like any From 5e81250aa12b04caa389d49b8e7f588bab4268c2 Mon Sep 17 00:00:00 2001 From: Nara Hahn Date: Wed, 6 Mar 2019 18:13:06 +0100 Subject: [PATCH 032/101] Add time-domain examples in docstring (#99) --- sfs/time/drivingfunction.py | 67 +++++++++++++++++++++++++++++++++++++ sfs/time/source.py | 38 +++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/sfs/time/drivingfunction.py b/sfs/time/drivingfunction.py index a6d2ae8..595612c 100644 --- a/sfs/time/drivingfunction.py +++ b/sfs/time/drivingfunction.py @@ -2,6 +2,43 @@ .. include:: math-definitions.rst +.. plot:: + :context: reset + + import matplotlib.pyplot as plt + import numpy as np + from scipy.signal import unit_impulse + import sfs + + # Plane wave + npw = sfs.util.direction_vector(np.radians(-45)) + + # Point source + xs = -1.5, 1.5, 0 + rs = np.linalg.norm(xs) # distance from origin + ts = rs / sfs.default.c # time-of-arrival at origin + + # Focused source + xf = -0.5, 0.5, 0 + nf = sfs.util.direction_vector(np.radians(-45)) # normal vector + rf = np.linalg.norm(xf) # distance from origin + tf = rf / sfs.default.c # time-of-arrival at origin + + # Impulsive excitation + signal = unit_impulse(512), 44100 + + # Circular loudspeaker array + N = 32 # number of loudspeakers + R = 1.5 # radius + x0, n0, a0 = sfs.array.circular(N, R) + + grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02) + + def plot(d, selected, t=0): + p = sfs.time.soundfield.p_array(x0, d, selected * a0, t, grid) + sfs.plot.level(p, grid) + sfs.plot.loudspeaker_2d(x0, n0, selected * a0, size=0.15) + """ import numpy as np from numpy.core.umath_tests import inner1d # element-wise inner product @@ -54,6 +91,16 @@ def wfs_25d_plane(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): ---------- See http://sfstoolbox.org/en/latest/#equation-d.wfs.pw.2.5D + Examples + -------- + .. plot:: + :context: close-figs + + delays, weights = sfs.time.drivingfunction.wfs_25d_plane(x0, n0, npw) + d = sfs.time.drivingfunction.driving_signals(delays, weights, signal) + a = sfs.mono.drivingfunction.source_selection_plane(n0, npw) + plot(d, a) + """ if c is None: c = default.c @@ -114,6 +161,16 @@ def wfs_25d_point(x0, n0, xs, xref=[0, 0, 0], c=None): ---------- See http://sfstoolbox.org/en/latest/#equation-d.wfs.ps.2.5D + Examples + -------- + .. plot:: + :context: close-figs + + delays, weights = sfs.time.drivingfunction.wfs_25d_point(x0, n0, xs) + d = sfs.time.drivingfunction.driving_signals(delays, weights, signal) + a = sfs.mono.drivingfunction.source_selection_point(n0, x0, xs) + plot(d, a, t=ts) + """ if c is None: c = default.c @@ -177,6 +234,16 @@ def wfs_25d_focused(x0, n0, xs, xref=[0, 0, 0], c=None): ---------- See http://sfstoolbox.org/en/latest/#equation-d.wfs.fs.2.5D + Examples + -------- + .. plot:: + :context: close-figs + + delays, weights = sfs.time.drivingfunction.wfs_25d_focused(x0, n0, xf) + d = sfs.time.drivingfunction.driving_signals(delays, weights, signal) + a = sfs.mono.drivingfunction.source_selection_focused(nf, x0, xf) + plot(d, a, t=tf) + """ if c is None: c = default.c diff --git a/sfs/time/source.py b/sfs/time/source.py index e9dab06..cfb6898 100644 --- a/sfs/time/source.py +++ b/sfs/time/source.py @@ -4,6 +4,23 @@ .. include:: math-definitions.rst +.. plot:: + :context: reset + + import matplotlib.pyplot as plt + import numpy as np + from scipy.signal import unit_impulse + import sfs + + xs = 1.5, 1, 0 # source position + rs = np.linalg.norm(xs) # distance from origin + ts = rs / sfs.default.c # time-of-arrival at origin + + # Impulsive excitation + signal = unit_impulse(512), 44100 + + grid = sfs.util.xyz_grid([-2, 3], [-1, 2], 0, spacing=0.02) + """ import numpy as np @@ -45,6 +62,14 @@ def point(xs, signal, observation_time, grid, c=None): g(x-x_s,t) = \frac{1}{4 \pi |x - x_s|} \dirac{t - \frac{|x - x_s|}{c}} + Examples + -------- + .. plot:: + :context: close-figs + + p = sfs.time.source.point(xs, signal, ts, grid) + sfs.plot.level(p, grid) + """ xs = util.asarray_1d(xs) data, samplerate, signal_offset = util.as_delayed_signal(signal) @@ -94,6 +119,19 @@ def point_image_sources(x0, signal, observation_time, grid, L, max_order, Scalar sound pressure field, evaluated at positions given by *grid*. + Examples + -------- + .. plot:: + :context: close-figs + + room = 5, 3, 1.5 # room dimensions + order = 2 # image source order + coeffs = .8, .8, .6, .6, .7, .7 # wall reflection coefficients + grid = sfs.util.xyz_grid([0, room[0]], [0, room[1]], 0, spacing=0.01) + p = sfs.time.source.point_image_sources( + xs, signal, 1.5 * ts, grid, room, order, coeffs) + sfs.plot.level(p, grid) + """ if coeffs is None: coeffs = np.ones(6) From e6db2f98b7eb9125e8f56f807eab341ba9e2e3e3 Mon Sep 17 00:00:00 2001 From: Nara Hahn Date: Wed, 6 Mar 2019 18:40:34 +0100 Subject: [PATCH 033/101] Adjust axis limits in sound field plots (#96) --- sfs/plot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sfs/plot.py b/sfs/plot.py index 4457848..2e96d21 100644 --- a/sfs/plot.py +++ b/sfs/plot.py @@ -283,6 +283,9 @@ def soundfield(p, grid, xnorm=None, cmap='coolwarm_clip', vmin=-2.0, vmax=2.0, elif plotting_plane == 'yz': x, y = grid[[1, 2]] + dx = 0.5 * x.ptp() / p.shape[0] + dy = 0.5 * y.ptp() / p.shape[1] + if ax is None: ax = plt.gca() @@ -291,7 +294,7 @@ def soundfield(p, grid, xnorm=None, cmap='coolwarm_clip', vmin=-2.0, vmax=2.0, p = np.clip(p, -1e15, 1e15) # clip to float64 range im = ax.imshow(np.real(p), cmap=cmap, origin='lower', - extent=[x.min(), x.max(), y.min(), y.max()], + extent=[x.min()-dx, x.max()+dx, y.min()-dy, y.max()+dy], vmax=vmax, vmin=vmin, **kwargs) if xlabel is None: xlabel = plotting_plane[0] + ' / m' From 3ae7a9ffdfa2775f0ce55b2d6c74727be0be4632 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Fri, 1 Mar 2019 14:32:33 +0100 Subject: [PATCH 034/101] DOC: Remove white margin around plots in API docs --- doc/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index bd4d959..dddcfb3 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -80,6 +80,9 @@ plot_html_show_source_link = False plot_html_show_formats = False plot_pre_code = '' +plot_rcparams = { + 'savefig.bbox': 'tight', +} plot_formats = ['svg', 'pdf'] mathjax_config = { From a96645a3332515343662852b64ac0d8a23820eeb Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 2 Mar 2019 18:49:43 +0100 Subject: [PATCH 035/101] TST: Run doctests on Travis-CI --- .travis.yml | 4 ++-- doc/conf.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8324188..e92c3ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,5 +18,5 @@ install: script: - python -m pytest - python doc/examples/run_all.py - # This executes the example notebooks: - - python -m sphinx doc/ _build/ -b dummy + # This executes the example notebooks and runs the doctests: + - python -m sphinx doc/ _build/ -b doctest diff --git a/doc/conf.py b/doc/conf.py index dddcfb3..8934794 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -39,6 +39,7 @@ 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', # support for NumPy-style docstrings 'sphinx.ext.intersphinx', + 'sphinx.ext.doctest', 'sphinxcontrib.bibtex', 'matplotlib.sphinxext.plot_directive', 'nbsphinx', From 843d53a778954872d096782aea152990ad477239 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 6 Mar 2019 19:41:46 +0100 Subject: [PATCH 036/101] Move source selection helpers to sfs.util --- doc/examples/horizontal_plane_arrays.py | 26 +++++----- doc/examples/sound_field_synthesis.py | 6 +-- doc/examples/time_domain.py | 6 +-- sfs/mono/drivingfunction.py | 65 +++---------------------- sfs/mono/soundfigure.py | 2 +- sfs/time/drivingfunction.py | 6 +-- sfs/util.py | 52 ++++++++++++++++++++ 7 files changed, 82 insertions(+), 81 deletions(-) diff --git a/doc/examples/horizontal_plane_arrays.py b/doc/examples/horizontal_plane_arrays.py index cb42a36..9ad8969 100644 --- a/doc/examples/horizontal_plane_arrays.py +++ b/doc/examples/horizontal_plane_arrays.py @@ -48,7 +48,7 @@ def compute_and_plot_soundfield(title): x0, n0, a0 = sfs.array.linear(N, dx, center=acenter, orientation=anormal) sourcetype = sfs.mono.source.point -a = sfs.mono.drivingfunction.source_selection_point(n0, x0, xs) +a = sfs.util.source_selection_point(n0, x0, xs) d = sfs.mono.drivingfunction.wfs_3d_point(omega, x0, n0, xs) compute_and_plot_soundfield('linear_ps_wfs_3d_point') @@ -67,7 +67,7 @@ def compute_and_plot_soundfield(title): # linear array, secondary point sources, virtual plane wave sourcetype = sfs.mono.source.point -a = sfs.mono.drivingfunction.source_selection_plane(n0, npw) +a = sfs.util.source_selection_plane(n0, npw) d = sfs.mono.drivingfunction.wfs_3d_plane(omega, x0, n0, npw) compute_and_plot_soundfield('linear_ps_wfs_3d_plane') @@ -84,11 +84,11 @@ def compute_and_plot_soundfield(title): center=acenter, orientation=anormal) d = sfs.mono.drivingfunction.wfs_25d_point(omega, x0, n0, xs, xref=xnorm) -a = sfs.mono.drivingfunction.source_selection_point(n0, x0, xs) +a = sfs.util.source_selection_point(n0, x0, xs) compute_and_plot_soundfield('linear_nested_ps_wfs_25d_point') d = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref=xnorm) -a = sfs.mono.drivingfunction.source_selection_plane(n0, npw) +a = sfs.util.source_selection_plane(n0, npw) compute_and_plot_soundfield('linear_nested_ps_wfs_25d_plane') @@ -97,22 +97,22 @@ def compute_and_plot_soundfield(title): orientation=anormal) d = sfs.mono.drivingfunction.wfs_25d_point(omega, x0, n0, xs, xref=xnorm) -a = sfs.mono.drivingfunction.source_selection_point(n0, x0, xs) +a = sfs.util.source_selection_point(n0, x0, xs) compute_and_plot_soundfield('linear_random_ps_wfs_25d_point') d = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref=xnorm) -a = sfs.mono.drivingfunction.source_selection_plane(n0, npw) +a = sfs.util.source_selection_plane(n0, npw) compute_and_plot_soundfield('linear_random_ps_wfs_25d_plane') # rectangular array, secondary point sources x0, n0, a0 = sfs.array.rectangular((N, N//2), dx, center=acenter, orientation=anormal) d = sfs.mono.drivingfunction.wfs_25d_point(omega, x0, n0, xs, xref=xnorm) -a = sfs.mono.drivingfunction.source_selection_point(n0, x0, xs) +a = sfs.util.source_selection_point(n0, x0, xs) compute_and_plot_soundfield('rectangular_ps_wfs_25d_point') d = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref=xnorm) -a = sfs.mono.drivingfunction.source_selection_plane(n0, npw) +a = sfs.util.source_selection_plane(n0, npw) compute_and_plot_soundfield('rectangular_ps_wfs_25d_plane') @@ -120,11 +120,11 @@ def compute_and_plot_soundfield(title): N = 60 x0, n0, a0 = sfs.array.circular(N, 1, center=acenter) d = sfs.mono.drivingfunction.wfs_25d_point(omega, x0, n0, xs, xref=xnorm) -a = sfs.mono.drivingfunction.source_selection_point(n0, x0, xs) +a = sfs.util.source_selection_point(n0, x0, xs) compute_and_plot_soundfield('circular_ps_wfs_25d_point') d = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref=xnorm) -a = sfs.mono.drivingfunction.source_selection_plane(n0, npw) +a = sfs.util.source_selection_plane(n0, npw) compute_and_plot_soundfield('circular_ps_wfs_25d_plane') @@ -135,7 +135,7 @@ def compute_and_plot_soundfield(title): sourcetype = sfs.mono.source.line d = sfs.mono.drivingfunction.nfchoa_2d_plane(omega, x0, 1, npw) -a = sfs.mono.drivingfunction.source_selection_all(N) +a = sfs.util.source_selection_all(N) compute_and_plot_soundfield('circular_ls_nfchoa_2d_plane') @@ -146,9 +146,9 @@ def compute_and_plot_soundfield(title): sourcetype = sfs.mono.source.point d = sfs.mono.drivingfunction.nfchoa_25d_point(omega, x0, 1, xs) -a = sfs.mono.drivingfunction.source_selection_all(N) +a = sfs.util.source_selection_all(N) compute_and_plot_soundfield('circular_ps_nfchoa_25d_point') d = sfs.mono.drivingfunction.nfchoa_25d_plane(omega, x0, 1, npw) -a = sfs.mono.drivingfunction.source_selection_all(N) +a = sfs.util.source_selection_all(N) compute_and_plot_soundfield('circular_ps_nfchoa_25d_plane') diff --git a/doc/examples/sound_field_synthesis.py b/doc/examples/sound_field_synthesis.py index 2dfef55..f6db41c 100644 --- a/doc/examples/sound_field_synthesis.py +++ b/doc/examples/sound_field_synthesis.py @@ -60,9 +60,9 @@ #d = sfs.mono.drivingfunction.nfchoa_25d_plane(omega, x0, R, npw) # === determine active secondary sources === -a = sfs.mono.drivingfunction.source_selection_plane(n0, npw) -#a = sfs.mono.drivingfunction.source_selection_point(n0, x0, xs) -#a = sfs.mono.drivingfunction.source_selection_all(len(x0)) +a = sfs.util.source_selection_plane(n0, npw) +#a = sfs.util.source_selection_point(n0, x0, xs) +#a = sfs.util.source_selection_all(len(x0)) # === compute tapering window === diff --git a/doc/examples/time_domain.py b/doc/examples/time_domain.py index 06c3374..9e09c5a 100644 --- a/doc/examples/time_domain.py +++ b/doc/examples/time_domain.py @@ -28,7 +28,7 @@ d = sfs.time.drivingfunction.driving_signals(d_delay, d_weight, signal) # test soundfield -a = sfs.mono.drivingfunction.source_selection_point(n0, x0, xs) +a = sfs.util.source_selection_point(n0, x0, xs) twin = sfs.tapering.tukey(a, .3) p = sfs.time.soundfield.p_array(x0, d, twin * a0, t, grid) p = p * 100 # scale absolute amplitude @@ -51,7 +51,7 @@ d = sfs.time.drivingfunction.driving_signals(d_delay, d_weight, signal) # test soundfield -a = sfs.mono.drivingfunction.source_selection_plane(n0, npw) +a = sfs.util.source_selection_plane(n0, npw) twin = sfs.tapering.tukey(a, .3) p = sfs.time.soundfield.p_array(x0, d, twin * a0, t, grid) @@ -72,7 +72,7 @@ d = sfs.time.drivingfunction.driving_signals(d_delay, d_weight, signal) # test soundfield -a = sfs.mono.drivingfunction.source_selection_focused(nfs, x0, xs) +a = sfs.util.source_selection_focused(nfs, x0, xs) twin = sfs.tapering.tukey(a, .3) p = sfs.time.soundfield.p_array(x0, d, twin * a0, t, grid) p = p * 100 # scale absolute amplitude diff --git a/sfs/mono/drivingfunction.py b/sfs/mono/drivingfunction.py index 001c579..aaf4a0a 100644 --- a/sfs/mono/drivingfunction.py +++ b/sfs/mono/drivingfunction.py @@ -54,7 +54,7 @@ def wfs_2d_line(omega, x0, n0, xs, c=None): :context: close-figs d = sfs.mono.drivingfunction.wfs_2d_line(omega, x0, n0, xs) - a = sfs.mono.drivingfunction.source_selection_line(n0, x0, xs) + a = sfs.util.source_selection_line(n0, x0, xs) plot(d, a) """ @@ -82,7 +82,7 @@ def _wfs_point(omega, x0, n0, xs, c=None): :context: close-figs d = sfs.mono.drivingfunction.wfs_3d_point(omega, x0, n0, xs) - a = sfs.mono.drivingfunction.source_selection_point(n0, x0, xs) + a = sfs.util.source_selection_point(n0, x0, xs) plot(d, a) """ @@ -114,7 +114,7 @@ def wfs_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): :context: close-figs d = sfs.mono.drivingfunction.wfs_25d_point(omega, x0, n0, xs) - a = sfs.mono.drivingfunction.source_selection_point(n0, x0, xs) + a = sfs.util.source_selection_point(n0, x0, xs) plot(d, a) """ @@ -150,7 +150,7 @@ def _wfs_plane(omega, x0, n0, n=[0, 1, 0], c=None): :context: close-figs d = sfs.mono.drivingfunction.wfs_3d_plane(omega, x0, n0, npw) - a = sfs.mono.drivingfunction.source_selection_plane(n0, npw) + a = sfs.util.source_selection_plane(n0, npw) plot(d, a) """ @@ -180,7 +180,7 @@ def wfs_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, :context: close-figs d = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw) - a = sfs.mono.drivingfunction.source_selection_plane(n0, npw) + a = sfs.util.source_selection_plane(n0, npw) plot(d, a) """ @@ -212,7 +212,7 @@ def _wfs_focused(omega, x0, n0, xs, c=None): :context: close-figs d = sfs.mono.drivingfunction.wfs_3d_focused(omega, x0, n0, xs_focused) - a = sfs.mono.drivingfunction.source_selection_focused(ns, x0, xs_focused) + a = sfs.util.source_selection_focused(ns, x0, xs_focused) plot(d, a) """ @@ -244,7 +244,7 @@ def wfs_25d_focused(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): :context: close-figs d = sfs.mono.drivingfunction.wfs_25d_focused(omega, x0, n0, xs_focused) - a = sfs.mono.drivingfunction.source_selection_focused(ns, x0, xs_focused) + a = sfs.util.source_selection_focused(ns, x0, xs_focused) plot(d, a) """ @@ -283,57 +283,6 @@ def delay_3d_plane(omega, x0, n0, n=[0, 1, 0], c=None): return np.exp(-1j * k * np.inner(n, x0)) -def source_selection_plane(n0, n): - """Secondary source selection for a plane wave. - - Eq.(13) from :cite:`Spors2008` - - """ - n0 = util.asarray_of_rows(n0) - n = util.normalize_vector(n) - return np.inner(n, n0) >= default.selection_tolerance - - -def source_selection_point(n0, x0, xs): - """Secondary source selection for a point source. - - Eq.(15) from :cite:`Spors2008` - - """ - n0 = util.asarray_of_rows(n0) - x0 = util.asarray_of_rows(x0) - xs = util.asarray_1d(xs) - ds = x0 - xs - return inner1d(ds, n0) >= default.selection_tolerance - - -def source_selection_line(n0, x0, xs): - """Secondary source selection for a line source. - - compare Eq.(15) from :cite:`Spors2008` - - """ - return source_selection_point(n0, x0, xs) - - -def source_selection_focused(ns, x0, xs): - """Secondary source selection for a focused source. - - Eq.(2.78) from :cite:`Wierstorf2014` - - """ - x0 = util.asarray_of_rows(x0) - xs = util.asarray_1d(xs) - ns = util.normalize_vector(ns) - ds = xs - x0 - return inner1d(ns, ds) >= default.selection_tolerance - - -def source_selection_all(N): - """Select all secondary sources.""" - return np.ones(N, dtype=bool) - - def nfchoa_2d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): r"""Plane wave by two-dimensional NFC-HOA. diff --git a/sfs/mono/soundfigure.py b/sfs/mono/soundfigure.py index 272bc4f..8167c9d 100644 --- a/sfs/mono/soundfigure.py +++ b/sfs/mono/soundfigure.py @@ -39,7 +39,7 @@ def wfs_3d_pw(omega, x0, n0, figure, npw=[0, 0, 1], c=None): npw = 1/k * np.asarray([kx[n], ky[m], kz]) npw = npw / np.linalg.norm(npw) # driving function of plane wave with positive kz - a = drivingfunction.source_selection_plane(n0, npw) + a = util.source_selection_plane(n0, npw) a = a * figure[n, m] d += a * drivingfunction.wfs_3d_plane(omega, x0, n0, npw, c) diff --git a/sfs/time/drivingfunction.py b/sfs/time/drivingfunction.py index 595612c..55bddf3 100644 --- a/sfs/time/drivingfunction.py +++ b/sfs/time/drivingfunction.py @@ -98,7 +98,7 @@ def wfs_25d_plane(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): delays, weights = sfs.time.drivingfunction.wfs_25d_plane(x0, n0, npw) d = sfs.time.drivingfunction.driving_signals(delays, weights, signal) - a = sfs.mono.drivingfunction.source_selection_plane(n0, npw) + a = sfs.util.source_selection_plane(n0, npw) plot(d, a) """ @@ -168,7 +168,7 @@ def wfs_25d_point(x0, n0, xs, xref=[0, 0, 0], c=None): delays, weights = sfs.time.drivingfunction.wfs_25d_point(x0, n0, xs) d = sfs.time.drivingfunction.driving_signals(delays, weights, signal) - a = sfs.mono.drivingfunction.source_selection_point(n0, x0, xs) + a = sfs.util.source_selection_point(n0, x0, xs) plot(d, a, t=ts) """ @@ -241,7 +241,7 @@ def wfs_25d_focused(x0, n0, xs, xref=[0, 0, 0], c=None): delays, weights = sfs.time.drivingfunction.wfs_25d_focused(x0, n0, xf) d = sfs.time.drivingfunction.driving_signals(delays, weights, signal) - a = sfs.mono.drivingfunction.source_selection_focused(nf, x0, xf) + a = sfs.util.source_selection_focused(nf, x0, xf) plot(d, a, t=tf) """ diff --git a/sfs/util.py b/sfs/util.py index 7c75ff8..61e3f0e 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -6,6 +6,7 @@ import collections import numpy as np +from numpy.core.umath_tests import inner1d from scipy.special import spherical_jn, spherical_yn from . import default @@ -564,3 +565,54 @@ def spherical_hn2(n, z): """ return spherical_jn(n, z) - 1j * spherical_yn(n, z) + + +def source_selection_plane(n0, n): + """Secondary source selection for a plane wave. + + Eq.(13) from :cite:`Spors2008` + + """ + n0 = asarray_of_rows(n0) + n = normalize_vector(n) + return np.inner(n, n0) >= default.selection_tolerance + + +def source_selection_point(n0, x0, xs): + """Secondary source selection for a point source. + + Eq.(15) from :cite:`Spors2008` + + """ + n0 = asarray_of_rows(n0) + x0 = asarray_of_rows(x0) + xs = asarray_1d(xs) + ds = x0 - xs + return inner1d(ds, n0) >= default.selection_tolerance + + +def source_selection_line(n0, x0, xs): + """Secondary source selection for a line source. + + compare Eq.(15) from :cite:`Spors2008` + + """ + return source_selection_point(n0, x0, xs) + + +def source_selection_focused(ns, x0, xs): + """Secondary source selection for a focused source. + + Eq.(2.78) from :cite:`Wierstorf2014` + + """ + x0 = asarray_of_rows(x0) + xs = asarray_1d(xs) + ns = normalize_vector(ns) + ds = xs - x0 + return inner1d(ns, ds) >= default.selection_tolerance + + +def source_selection_all(N): + """Select all secondary sources.""" + return np.ones(N, dtype=bool) From d95e371f3c0955a052a8684fd583f7fbe7c199d4 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 2 Mar 2019 17:24:59 +0100 Subject: [PATCH 037/101] Introduce synthesize() functions for soundfield superposition --- doc/examples/horizontal_plane_arrays.py | 95 ++++++------ doc/examples/mirror-image-source-model.ipynb | 2 +- doc/examples/modal-room-acoustics.ipynb | 5 +- doc/examples/plot_particle_density.py | 4 +- doc/examples/sound_field_synthesis.py | 60 ++++---- doc/examples/soundfigures.py | 8 +- doc/examples/time_domain.py | 40 ++--- sfs/array.py | 93 ++++++++---- sfs/mono/__init__.py | 49 +++++- sfs/mono/drivingfunction.py | 149 ++++++++++++------- sfs/mono/soundfigure.py | 10 +- sfs/mono/source.py | 44 ++---- sfs/mono/synthesized.py | 22 --- sfs/time/__init__.py | 56 ++++++- sfs/time/drivingfunction.py | 72 ++++++--- sfs/time/soundfield.py | 55 ------- tests/test_array.py | 6 +- 17 files changed, 444 insertions(+), 326 deletions(-) delete mode 100644 sfs/mono/synthesized.py delete mode 100644 sfs/time/soundfield.py diff --git a/doc/examples/horizontal_plane_arrays.py b/doc/examples/horizontal_plane_arrays.py index 9ad8969..75b07f9 100644 --- a/doc/examples/horizontal_plane_arrays.py +++ b/doc/examples/horizontal_plane_arrays.py @@ -29,14 +29,13 @@ def compute_and_plot_soundfield(title): """Compute and plot synthesized sound field.""" print('Computing', title) - twin = tapering(a, talpha) - p = sfs.mono.synthesized.generic(omega, x0, n0, d * twin * a0, grid, - source=sourcetype) + twin = tapering(selection, talpha) + p = sfs.mono.synthesize(d, twin, array, secondary_source, grid=grid) plt.figure(figsize=(15, 15)) plt.cla() sfs.plot.soundfield(p, grid, xnorm) - sfs.plot.loudspeaker_2d(x0, n0, twin) + sfs.plot.loudspeaker_2d(array.x, array.n, twin) sfs.plot.virtualsource_2d(xs) sfs.plot.virtualsource_2d([0, 0], npw, type='plane') plt.title(title) @@ -45,110 +44,108 @@ def compute_and_plot_soundfield(title): # linear array, secondary point sources, virtual monopole -x0, n0, a0 = sfs.array.linear(N, dx, center=acenter, orientation=anormal) +array = sfs.array.linear(N, dx, center=acenter, orientation=anormal) -sourcetype = sfs.mono.source.point -a = sfs.util.source_selection_point(n0, x0, xs) - -d = sfs.mono.drivingfunction.wfs_3d_point(omega, x0, n0, xs) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_3d_point( + omega, array.x, array.n, xs) compute_and_plot_soundfield('linear_ps_wfs_3d_point') -d = sfs.mono.drivingfunction.wfs_25d_point(omega, x0, n0, xs, xref=xnorm) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_point( + omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('linear_ps_wfs_25d_point') -d = sfs.mono.drivingfunction.wfs_2d_point(omega, x0, n0, xs) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_2d_point( + omega, array.x, array.n, xs) compute_and_plot_soundfield('linear_ps_wfs_2d_point') # linear array, secondary line sources, virtual line source -sourcetype = sfs.mono.source.line -d = sfs.mono.drivingfunction.wfs_2d_line(omega, x0, n0, xs) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_2d_line( + omega, array.x, array.n, xs) compute_and_plot_soundfield('linear_ls_wfs_2d_line') # linear array, secondary point sources, virtual plane wave -sourcetype = sfs.mono.source.point -a = sfs.util.source_selection_plane(n0, npw) - -d = sfs.mono.drivingfunction.wfs_3d_plane(omega, x0, n0, npw) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_3d_plane( + omega, array.x, array.n, npw) compute_and_plot_soundfield('linear_ps_wfs_3d_plane') -d = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref=xnorm) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_plane( + omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('linear_ps_wfs_25d_plane') -d = sfs.mono.drivingfunction.wfs_2d_plane(omega, x0, n0, npw) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_2d_plane( + omega, array.x, array.n, npw) compute_and_plot_soundfield('linear_ps_wfs_2d_plane') # non-uniform linear array, secondary point sources -x0, n0, a0 = sfs.array.linear_diff(N//3 * [dx] + N//3 * [dx/2] + N//3 * [dx], +array = sfs.array.linear_diff(N//3 * [dx] + N//3 * [dx/2] + N//3 * [dx], center=acenter, orientation=anormal) -d = sfs.mono.drivingfunction.wfs_25d_point(omega, x0, n0, xs, xref=xnorm) -a = sfs.util.source_selection_point(n0, x0, xs) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_point( + omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('linear_nested_ps_wfs_25d_point') -d = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref=xnorm) -a = sfs.util.source_selection_plane(n0, npw) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_plane( + omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('linear_nested_ps_wfs_25d_plane') # random sampled linear array, secondary point sources -x0, n0, a0 = sfs.array.linear_random(N, dx/2, 1.5*dx, center=acenter, +array = sfs.array.linear_random(N, dx/2, 1.5*dx, center=acenter, orientation=anormal) -d = sfs.mono.drivingfunction.wfs_25d_point(omega, x0, n0, xs, xref=xnorm) -a = sfs.util.source_selection_point(n0, x0, xs) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_point( + omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('linear_random_ps_wfs_25d_point') -d = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref=xnorm) -a = sfs.util.source_selection_plane(n0, npw) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_plane( + omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('linear_random_ps_wfs_25d_plane') # rectangular array, secondary point sources -x0, n0, a0 = sfs.array.rectangular((N, N//2), dx, center=acenter, orientation=anormal) -d = sfs.mono.drivingfunction.wfs_25d_point(omega, x0, n0, xs, xref=xnorm) -a = sfs.util.source_selection_point(n0, x0, xs) +array = sfs.array.rectangular((N, N//2), dx, center=acenter, orientation=anormal) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_point( + omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('rectangular_ps_wfs_25d_point') -d = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref=xnorm) -a = sfs.util.source_selection_plane(n0, npw) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_plane( + omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('rectangular_ps_wfs_25d_plane') # circular array, secondary point sources N = 60 -x0, n0, a0 = sfs.array.circular(N, 1, center=acenter) -d = sfs.mono.drivingfunction.wfs_25d_point(omega, x0, n0, xs, xref=xnorm) -a = sfs.util.source_selection_point(n0, x0, xs) +array = sfs.array.circular(N, 1, center=acenter) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_point( + omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('circular_ps_wfs_25d_point') -d = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref=xnorm) -a = sfs.util.source_selection_plane(n0, npw) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_plane( + omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('circular_ps_wfs_25d_plane') # circular array, secondary line sources, NFC-HOA -x0, n0, a0 = sfs.array.circular(N, 1) +array = sfs.array.circular(N, 1) xnorm = [0, 0, 0] talpha = 0 # switches off tapering -sourcetype = sfs.mono.source.line -d = sfs.mono.drivingfunction.nfchoa_2d_plane(omega, x0, 1, npw) -a = sfs.util.source_selection_all(N) +d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_2d_plane( + omega, array.x, 1, npw) compute_and_plot_soundfield('circular_ls_nfchoa_2d_plane') # circular array, secondary point sources, NFC-HOA -x0, n0, a0 = sfs.array.circular(N, 1) +array = sfs.array.circular(N, 1) xnorm = [0, 0, 0] talpha = 0 # switches off tapering -sourcetype = sfs.mono.source.point -d = sfs.mono.drivingfunction.nfchoa_25d_point(omega, x0, 1, xs) -a = sfs.util.source_selection_all(N) +d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_25d_point( + omega, array.x, 1, xs) compute_and_plot_soundfield('circular_ps_nfchoa_25d_point') -d = sfs.mono.drivingfunction.nfchoa_25d_plane(omega, x0, 1, npw) -a = sfs.util.source_selection_all(N) +d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_25d_plane( + omega, array.x, 1, npw) compute_and_plot_soundfield('circular_ps_nfchoa_25d_plane') diff --git a/doc/examples/mirror-image-source-model.ipynb b/doc/examples/mirror-image-source-model.ipynb index eb61663..a20e93f 100644 --- a/doc/examples/mirror-image-source-model.ipynb +++ b/doc/examples/mirror-image-source-model.ipynb @@ -93,7 +93,7 @@ "outputs": [], "source": [ "grid = sfs.util.xyz_grid([0, L[0]], [0, L[1]], 1.5, spacing=0.02)\n", - "P = sfs.mono.source.point_image_sources(omega, x0, [1, 0, 0], grid, L,\n", + "P = sfs.mono.source.point_image_sources(omega, x0, grid, L,\n", " max_order, coeffs=coeffs)" ] }, diff --git a/doc/examples/modal-room-acoustics.ipynb b/doc/examples/modal-room-acoustics.ipynb index 1468b37..6427119 100644 --- a/doc/examples/modal-room-acoustics.ipynb +++ b/doc/examples/modal-room-acoustics.ipynb @@ -36,7 +36,6 @@ "x0 = 1, 3, 1.80 # source position\n", "L = 6, 6, 3 # dimensions of room\n", "deltan = 0.01 # absorption factor of walls\n", - "n0 = 1, 0, 0 # normal vector of source (only for compatibility)\n", "N = 20 # maximum order of modes" ] }, @@ -88,7 +87,7 @@ "metadata": {}, "outputs": [], "source": [ - "p = sfs.mono.source.point_modal(omega, x0, n0, grid, L, N=N, deltan=deltan)" + "p = sfs.mono.source.point_modal(omega, x0, grid, L, N=N, deltan=deltan)" ] }, { @@ -136,7 +135,7 @@ "\n", "receiver = 1, 1, 1.8\n", "\n", - "p = [sfs.mono.source.point_modal(om, x0, n0, receiver, L, N=N, deltan=deltan)\n", + "p = [sfs.mono.source.point_modal(om, x0, receiver, L, N=N, deltan=deltan)\n", " for om in omega]\n", " \n", "plt.plot(f, sfs.util.db(p))\n", diff --git a/doc/examples/plot_particle_density.py b/doc/examples/plot_particle_density.py index 1322fe0..f5b15e3 100644 --- a/doc/examples/plot_particle_density.py +++ b/doc/examples/plot_particle_density.py @@ -31,12 +31,12 @@ def plot_particle_displacement(title): # point source -v = sfs.mono.source.point_velocity(omega, xs, npw, grid) +v = sfs.mono.source.point_velocity(omega, xs, grid) amplitude = 1.5e6 plot_particle_displacement('particle_displacement_point_source') # line source -v = sfs.mono.source.line_velocity(omega, xs, npw, grid) +v = sfs.mono.source.line_velocity(omega, xs, grid) amplitude = 1.3e6 plot_particle_displacement('particle_displacement_line_source') diff --git a/doc/examples/sound_field_synthesis.py b/doc/examples/sound_field_synthesis.py index f6db41c..d680286 100644 --- a/doc/examples/sound_field_synthesis.py +++ b/doc/examples/sound_field_synthesis.py @@ -25,63 +25,57 @@ # angular frequency omega = 2 * np.pi * f # normal vector of plane wave -npw = sfs.util.direction_vector(np.radians(pw_angle), np.radians(90)) +npw = sfs.util.direction_vector(np.radians(pw_angle)) # === get secondary source positions === -#x0, n0, a0 = sfs.array.linear(N, dx, center=[-1, 0, 0]) -#x0, n0, a0 = sfs.array.linear_random(N, 0.2*dx, 5*dx) -#x0, n0, a0 = sfs.array.rectangular(N, dx, orientation=sfs.util.direction_vector(0*np.pi/4, np.pi/2)) -#x0, n0, a0 = sfs.array.circular(N, R) -x0, n0, a0 = sfs.array.load('../../data/arrays/university_rostock.csv') +#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') -#x0, n0, a0 = sfs.array.planar(N, dx, orientation=sfs.util.direction_vector(np.radians(0),np.radians(180))) -#x0, n0, a0 = sfs.array.cube(N, dx, orientation=sfs.util.direction_vector(0, np.pi/2)) +#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)) -#x0, n0, a0 = sfs.array.sphere_load('/Users/spors/Documents/src/SFS/data/spherical_grids/equally_spaced_points/006561points.mat', 1, center=[.5,0,0]) +#array = sfs.array.sphere_load('/Users/spors/Documents/src/SFS/data/spherical_grids/equally_spaced_points/006561points.mat', 1, center=[.5,0,0]) -# === compute driving function === -#d = sfs.mono.drivingfunction.delay_3d_plane(omega, x0, n0, npw) +# === compute driving function and determine active secondary sources === +#d, selection, secondary_source = sfs.mono.drivingfunction.delay_3d_plane(omega, array.x, array.n, npw) -#d = sfs.mono.drivingfunction.wfs_2d_line(omega, x0, n0, xs) +#d, selection, secondary_source = sfs.mono.drivingfunction.wfs_2d_line(omega, array.x, array.n, xs) -#d = sfs.mono.drivingfunction.wfs_2d_plane(omega, x0, n0, npw) -d = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref) -#d = sfs.mono.drivingfunction.wfs_3d_plane(omega, x0, n0, npw) +#d, selection, secondary_source = sfs.mono.drivingfunction.wfs_2d_plane(omega, array.x, array.n, npw) +d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_plane(omega, array.x, array.n, npw, xref) +#d, selection, secondary_source = sfs.mono.drivingfunction.wfs_3d_plane(omega, array.x, array.n, npw) -#d = sfs.mono.drivingfunction.wfs_2d_point(omega, x0, n0, xs) -#d = sfs.mono.drivingfunction.wfs_25d_point(omega, x0, n0, xs) -#d = sfs.mono.drivingfunction.wfs_3d_point(omega, x0, n0, xs) +#d, selection, secondary_source = sfs.mono.drivingfunction.wfs_2d_point(omega, array.x, array.n, xs) +#d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_point(omega, array.x, array.n, xs) +#d, selection, secondary_source = sfs.mono.drivingfunction.wfs_3d_point(omega, array.x, array.n, xs) -#d = sfs.mono.drivingfunction.nfchoa_2d_plane(omega, x0, R, npw) +#d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_2d_plane(omega, array.x, R, npw) -#d = sfs.mono.drivingfunction.nfchoa_25d_point(omega, x0, R, xs) -#d = sfs.mono.drivingfunction.nfchoa_25d_plane(omega, x0, R, npw) - -# === determine active secondary sources === -a = sfs.util.source_selection_plane(n0, npw) -#a = sfs.util.source_selection_point(n0, x0, xs) -#a = sfs.util.source_selection_all(len(x0)) +#d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_25d_point(omega, array.x, R, xs) +#d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_25d_plane(omega, array.x, R, npw) # === compute tapering window === -#twin = sfs.tapering.none(a) -#twin = sfs.tapering.kaiser(a, 8.6) -twin = sfs.tapering.tukey(a,.3) +#twin = sfs.tapering.none(selection) +#twin = sfs.tapering.kaiser(selection, 8.6) +twin = sfs.tapering.tukey(selection, 0.3) # === compute synthesized sound field === -p = sfs.mono.synthesized.generic(omega, x0, n0, d * twin * a0 , grid, - source=sfs.mono.source.point) +p = sfs.mono.synthesize(d, twin, array, secondary_source, grid=grid) # === plot synthesized sound field === plt.figure(figsize=(10, 10)) sfs.plot.soundfield(p, grid, [0, 0, 0]) -sfs.plot.loudspeaker_2d(x0, n0, twin) +sfs.plot.loudspeaker_2d(array.x, array.n, twin) plt.grid() plt.savefig('soundfield.png') -#sfs.plot.loudspeaker_3d(x0, n0, twin) +#sfs.plot.loudspeaker_3d(array.x, array.n, twin) #plt.savefig('loudspeakers.png') diff --git a/doc/examples/soundfigures.py b/doc/examples/soundfigures.py index e337789..e6a76a6 100644 --- a/doc/examples/soundfigures.py +++ b/doc/examples/soundfigures.py @@ -24,16 +24,16 @@ grid = sfs.util.xyz_grid([-3, 3], [-3, 3], 0, spacing=0.02) # get secondary source positions -x0, n0, a0 = sfs.array.cube(N, dx) +array = sfs.array.cube(N, dx) # driving function for sound figure figure = np.array(Image.open('figures/tree.png')) # read image from file figure = np.rot90(figure) # turn 0deg to the top -d = sfs.mono.soundfigure.wfs_3d_pw(omega, x0, n0, figure, npw=npw) +d, selection, secondary_source = sfs.mono.soundfigure.wfs_3d_pw( + omega, array.x, array.n, figure, npw=npw) # compute synthesized sound field -p = sfs.mono.synthesized.generic(omega, x0, n0, d * a0, grid, - source=sfs.mono.source.point) +p = sfs.mono.synthesize(d, selection, array, secondary_source, grid=grid) # plot and save synthesized sound field plt.figure(figsize=(10, 10)) diff --git a/doc/examples/time_domain.py b/doc/examples/time_domain.py index 9e09c5a..953769f 100644 --- a/doc/examples/time_domain.py +++ b/doc/examples/time_domain.py @@ -14,28 +14,30 @@ my_cmap = 'YlOrRd' N = 56 # number of secondary sources R = 1.5 # radius of spherical/circular array -x0, n0, a0 = sfs.array.circular(N, R) # get secondary source positions +array = sfs.array.circular(N, R) # get secondary source positions fs = 44100 # sampling rate # unit impulse signal = [1], fs # POINT SOURCE -xs = [2, 2, 0] # position of virtual source +xs = 2, 2, 0 # position of virtual source t = 0.008 # compute driving signals -d_delay, d_weight = sfs.time.drivingfunction.wfs_25d_point(x0, n0, xs) +d_delay, d_weight, selection, secondary_source = \ + sfs.time.drivingfunction.wfs_25d_point(array.x, array.n, xs) d = sfs.time.drivingfunction.driving_signals(d_delay, d_weight, signal) # test soundfield -a = sfs.util.source_selection_point(n0, x0, xs) -twin = sfs.tapering.tukey(a, .3) -p = sfs.time.soundfield.p_array(x0, d, twin * a0, t, grid) +twin = sfs.tapering.tukey(selection, .3) + +p = sfs.time.synthesize(d, twin, array, + secondary_source, observation_time=t, grid=grid) p = p * 100 # scale absolute amplitude plt.figure(figsize=(10, 10)) sfs.plot.level(p, grid, cmap=my_cmap) -sfs.plot.loudspeaker_2d(x0, n0, twin) +sfs.plot.loudspeaker_2d(array.x, array.n, twin) plt.grid() sfs.plot.virtualsource_2d(xs) plt.title('impulse_ps_wfs_25d') @@ -47,17 +49,18 @@ t = -0.001 # compute driving signals -d_delay, d_weight = sfs.time.drivingfunction.wfs_25d_plane(x0, n0, npw) +d_delay, d_weight, selection, secondary_source = \ + sfs.time.drivingfunction.wfs_25d_plane(array.x, array.n, npw) d = sfs.time.drivingfunction.driving_signals(d_delay, d_weight, signal) # test soundfield -a = sfs.util.source_selection_plane(n0, npw) -twin = sfs.tapering.tukey(a, .3) -p = sfs.time.soundfield.p_array(x0, d, twin * a0, t, grid) +twin = sfs.tapering.tukey(selection, .3) +p = sfs.time.synthesize(d, twin, array, + secondary_source, observation_time=t, grid=grid) plt.figure(figsize=(10, 10)) sfs.plot.level(p, grid, cmap=my_cmap) -sfs.plot.loudspeaker_2d(x0, n0, twin) +sfs.plot.loudspeaker_2d(array.x, array.n, twin) plt.grid() sfs.plot.virtualsource_2d([0, 0], npw, type='plane') plt.title('impulse_pw_wfs_25d') @@ -68,21 +71,20 @@ xref = np.r_[0, 0, 0] nfs = sfs.util.normalize_vector(xref - xs) # main n of fsource t = 0.003 # compute driving signals -d_delay, d_weight = sfs.time.drivingfunction.wfs_25d_focused(x0, n0, xs) +d_delay, d_weight, selection, secondary_source = \ + sfs.time.drivingfunction.wfs_25d_focused(array.x, array.n, xs, nfs) d = sfs.time.drivingfunction.driving_signals(d_delay, d_weight, signal) # test soundfield -a = sfs.util.source_selection_focused(nfs, x0, xs) -twin = sfs.tapering.tukey(a, .3) -p = sfs.time.soundfield.p_array(x0, d, twin * a0, t, grid) +twin = sfs.tapering.tukey(selection, .3) +p = sfs.time.synthesize(d, twin, array, + secondary_source, observation_time=t, grid=grid) p = p * 100 # scale absolute amplitude plt.figure(figsize=(10, 10)) sfs.plot.level(p, grid, cmap=my_cmap) -sfs.plot.loudspeaker_2d(x0, n0, twin) +sfs.plot.loudspeaker_2d(array.x, array.n, twin) plt.grid() sfs.plot.virtualsource_2d(xs) plt.title('impulse_fs_wfs_25d') plt.savefig('impulse_fs_wfs_25d.png') - -# plt.show() diff --git a/sfs/array.py b/sfs/array.py index 8b0fe80..91f3b76 100644 --- a/sfs/array.py +++ b/sfs/array.py @@ -14,7 +14,8 @@ from . import util -class ArrayData(namedtuple('ArrayData', 'x n a')): +class SecondarySourceDistribution(namedtuple('SecondarySourceDistribution', + 'x n a')): """Named tuple returned by array functions. See `collections.namedtuple`. @@ -33,13 +34,54 @@ class ArrayData(namedtuple('ArrayData', 'x n a')): __slots__ = () def __repr__(self): - return 'ArrayData(\n' + ',\n'.join( + return 'SecondarySourceDistribution(\n' + ',\n'.join( ' {}={}'.format(name, repr(data).replace('\n', '\n ')) for name, data in zip('xna', self)) + ')' def take(self, indices): """Return a sub-array given by *indices*.""" - return ArrayData(self.x[indices], self.n[indices], self.a[indices]) + return SecondarySourceDistribution( + self.x[indices], self.n[indices], self.a[indices]) + + +def as_secondary_source_distribution(arg, **kwargs): + r"""Create a `SecondarySourceDistribution`. + + Parameters + ---------- + arg : sequence of between 1 and 3 array_like objects + All elements are converted to NumPy arrays. + If only 1 element is given, all normal vectors are set to *NaN*. + If only 1 or 2 elements are given, all weights are set to ``1.0``. + **kwargs + All keyword arguments are forwarded to :func:`numpy.asarray`. + + Returns + ------- + `SecondarySourceDistribution` + A named tuple consisting of three `numpy.ndarray`\s containing + positions, normal vectors and weights. + + """ + if len(arg) == 3: + x, n, a = arg + elif len(arg) == 2: + x, n = arg + a = 1.0 + elif len(arg) == 1: + x, = arg + n = np.nan, np.nan, np.nan + a = 1.0 + else: + raise TypeError('Between 1 and 3 elements are required') + x = util.asarray_of_rows(x, **kwargs) + n = util.asarray_of_rows(n, **kwargs) + if len(n) == 1: + n = np.tile(n, (len(x), 1)) + a = util.asarray_1d(a, **kwargs) + if len(a) == 1: + a = np.tile(a, len(x)) + return SecondarySourceDistribution(x, n, a) def linear(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): @@ -59,7 +101,7 @@ def linear(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): Returns ------- - `ArrayData` + `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. Examples @@ -87,7 +129,7 @@ def linear_diff(distances, center=[0, 0, 0], orientation=[1, 0, 0]): Returns ------- - `ArrayData` + `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. Examples @@ -124,7 +166,7 @@ def linear_random(N, min_spacing, max_spacing, center=[0, 0, 0], Returns ------- - `ArrayData` + `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. Examples @@ -156,7 +198,7 @@ def circular(N, R, center=[0, 0, 0]): Returns ------- - `ArrayData` + `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. Examples @@ -179,7 +221,7 @@ def circular(N, R, center=[0, 0, 0]): normals[:, 0] = np.cos(alpha + np.pi) normals[:, 1] = np.sin(alpha + np.pi) weights = np.ones(N) * 2 * np.pi * R / N - return ArrayData(positions, normals, weights) + return SecondarySourceDistribution(positions, normals, weights) def rectangular(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): @@ -200,7 +242,7 @@ def rectangular(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): Returns ------- - `ArrayData` + `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. Examples @@ -225,7 +267,7 @@ def rectangular(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): positions, normals = _rotate_array(positions, normals, [1, 0, 0], orientation) positions += center - return ArrayData(positions, normals, weights) + return SecondarySourceDistribution(positions, normals, weights) def rounded_edge(Nxy, Nr, dx, center=[0, 0, 0], orientation=[1, 0, 0]): @@ -245,7 +287,7 @@ def rounded_edge(Nxy, Nr, dx, center=[0, 0, 0], orientation=[1, 0, 0]): Returns ------- - `ArrayData` + `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. Examples @@ -297,7 +339,7 @@ def rounded_edge(Nxy, Nr, dx, center=[0, 0, 0], orientation=[1, 0, 0]): [1, 0, 0], orientation) # shift array to desired position positions += center - return ArrayData(positions, directions, weights) + return SecondarySourceDistribution(positions, directions, weights) def edge(Nxy, dx, center=[0, 0, 0], orientation=[1, 0, 0]): @@ -314,7 +356,7 @@ def edge(Nxy, dx, center=[0, 0, 0], orientation=[1, 0, 0]): Returns ------- - `ArrayData` + `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. Examples @@ -347,7 +389,7 @@ def edge(Nxy, dx, center=[0, 0, 0], orientation=[1, 0, 0]): [1, 0, 0], orientation) # shift array to desired position positions += center - return ArrayData(positions, directions, weights) + return SecondarySourceDistribution(positions, directions, weights) def planar(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): @@ -367,7 +409,7 @@ def planar(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): Returns ------- - `ArrayData` + `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. """ @@ -380,7 +422,7 @@ def planar(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): positions, normals = _rotate_array(positions, normals, [1, 0, 0], orientation) positions += center - return ArrayData(positions, normals, weights) + return SecondarySourceDistribution(positions, normals, weights) def cube(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): @@ -400,7 +442,7 @@ def cube(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): Returns ------- - `ArrayData` + `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. """ @@ -419,7 +461,7 @@ def cube(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): positions, directions = _rotate_array(positions, directions, [1, 0, 0], orientation) positions += center - return ArrayData(positions, directions, weights) + return SecondarySourceDistribution(positions, directions, weights) def sphere_load(fname, radius, center=[0, 0, 0]): @@ -430,7 +472,7 @@ def sphere_load(fname, radius, center=[0, 0, 0]): Returns ------- - `ArrayData` + `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. """ @@ -439,7 +481,7 @@ def sphere_load(fname, radius, center=[0, 0, 0]): normals = -positions positions *= radius positions += center - return ArrayData(positions, normals, weights) + return SecondarySourceDistribution(positions, normals, weights) def load(fname, center=[0, 0, 0], orientation=[1, 0, 0]): @@ -450,7 +492,7 @@ def load(fname, center=[0, 0, 0], orientation=[1, 0, 0]): Returns ------- - `ArrayData` + `SecondarySourceDistribution` Positions, orientations and weights of secondary sources. """ @@ -459,7 +501,7 @@ def load(fname, center=[0, 0, 0], orientation=[1, 0, 0]): positions, normals = _rotate_array(positions, normals, [1, 0, 0], orientation) positions += center - return ArrayData(positions, normals, weights) + return SecondarySourceDistribution(positions, normals, weights) def weights_midpoint(positions, closed): @@ -514,9 +556,10 @@ def _linear_helper(ycoordinates, center, orientation): positions += center normals = np.tile(normals, (N, 1)) weights = weights_midpoint(positions, closed=False) - return ArrayData(positions, normals, weights) + return SecondarySourceDistribution(positions, normals, weights) def concatenate(*arrays): - """Concatenate `ArrayData` objects.""" - return ArrayData._make(np.concatenate(i) for i in zip(*arrays)) + """Concatenate `SecondarySourceDistribution` objects.""" + return SecondarySourceDistribution._make(np.concatenate(i) + for i in zip(*arrays)) diff --git a/sfs/mono/__init__.py b/sfs/mono/__init__.py index 6f3ee6d..e33502e 100644 --- a/sfs/mono/__init__.py +++ b/sfs/mono/__init__.py @@ -5,11 +5,56 @@ drivingfunction source - synthesized soundfigure """ +import numpy as _np + from . import drivingfunction from . import source -from . import synthesized from . import soundfigure + +from .. import array as _array + + +def shiftphase(p, phase): + """Shift phase of a sound field.""" + p = _np.asarray(p) + return p * _np.exp(1j * phase) + + +def synthesize(d, weights, ssd, secondary_source_function, **kwargs): + """Compute sound field for a generic driving function. + + Parameters + ---------- + d : array_like + Driving function. + weights : array_like + Additional weights applied during integration, e.g. source + selection and tapering. + ssd : sequence of between 1 and 3 array_like objects + Positions, normal vectors and weights of secondary sources. + A `SecondarySourceDistribution` can also be used. + secondary_source_function : callable + A function that generates the sound field of a secondary source. + This signature is expected:: + + secondary_source_function( + position, normal_vector, weight, driving_function_weight, + **kwargs) -> numpy.ndarray + + **kwargs + All keyword arguments are forwarded to *secondary_source_function*. + This is typically used to pass the *grid* argument. + + """ + ssd = _array.as_secondary_source_distribution(ssd) + if not (len(ssd.x) == len(ssd.n) == len(ssd.a) == len(d) == + len(weights)): + raise ValueError("length mismatch") + p = 0 + for x, n, a, d, weight in zip(ssd.x, ssd.n, ssd.a, d, weights): + if weight != 0: + p += a * weight * d * secondary_source_function(x, n, **kwargs) + return p diff --git a/sfs/mono/drivingfunction.py b/sfs/mono/drivingfunction.py index aaf4a0a..fd188ca 100644 --- a/sfs/mono/drivingfunction.py +++ b/sfs/mono/drivingfunction.py @@ -16,19 +16,19 @@ # normal vector for plane wave: npw = sfs.util.direction_vector(np.radians(-45)) # normal vector for focused source: - ns = sfs.util.direction_vector(np.radians(-45)) + ns_focused = sfs.util.direction_vector(np.radians(-45)) f = 300 # Hz omega = 2 * np.pi * f R = 1.5 # Radius of circular loudspeaker array grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02) - x0, n0, a0 = sfs.array.circular(N=32, R=R) + array = sfs.array.circular(N=32, R=R) - def plot(d, selected): - p = sfs.mono.synthesized.generic(omega, x0, n0, d * selected * a0 , grid) + def plot(d, selection, secondary_source): + p = sfs.mono.synthesize(d, selection, array, secondary_source, grid=grid) sfs.plot.soundfield(p, grid) - sfs.plot.loudspeaker_2d(x0, n0, selected * a0, size=0.15) + sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) """ @@ -37,6 +37,7 @@ def plot(d, selected): from scipy.special import jn, hankel2 from .. import util from .. import default +from . import source as _source def wfs_2d_line(omega, x0, n0, xs, c=None): @@ -53,9 +54,9 @@ def wfs_2d_line(omega, x0, n0, xs, c=None): .. plot:: :context: close-figs - d = sfs.mono.drivingfunction.wfs_2d_line(omega, x0, n0, xs) - a = sfs.util.source_selection_line(n0, x0, xs) - plot(d, a) + d, selection, secondary_source = sfs.mono.drivingfunction.wfs_2d_line( + omega, array.x, array.n, xs) + plot(d, selection, secondary_source) """ x0 = util.asarray_of_rows(x0) @@ -64,7 +65,9 @@ def wfs_2d_line(omega, x0, n0, xs, c=None): k = util.wavenumber(omega, c) ds = x0 - xs r = np.linalg.norm(ds, axis=1) - return -1j/2 * k * inner1d(ds, n0) / r * hankel2(1, k * r) + d = -1j/2 * k * inner1d(ds, n0) / r * hankel2(1, k * r) + selection = util.source_selection_line(n0, x0, xs) + return d, selection, secondary_source_line(omega, c) def _wfs_point(omega, x0, n0, xs, c=None): @@ -81,9 +84,9 @@ def _wfs_point(omega, x0, n0, xs, c=None): .. plot:: :context: close-figs - d = sfs.mono.drivingfunction.wfs_3d_point(omega, x0, n0, xs) - a = sfs.util.source_selection_point(n0, x0, xs) - plot(d, a) + d, selection, secondary_source = sfs.mono.drivingfunction.wfs_3d_point( + omega, array.x, array.n, xs) + plot(d, selection, secondary_source) """ x0 = util.asarray_of_rows(x0) @@ -92,7 +95,9 @@ def _wfs_point(omega, x0, n0, xs, c=None): k = util.wavenumber(omega, c) ds = x0 - xs r = np.linalg.norm(ds, axis=1) - return 1j * k * inner1d(ds, n0) / r ** (3 / 2) * np.exp(-1j * k * r) + d = 1j * k * inner1d(ds, n0) / r ** (3 / 2) * np.exp(-1j * k * r) + selection = util.source_selection_point(n0, x0, xs) + return d, selection, secondary_source_point(omega, c) wfs_2d_point = _wfs_point @@ -113,9 +118,9 @@ def wfs_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): .. plot:: :context: close-figs - d = sfs.mono.drivingfunction.wfs_25d_point(omega, x0, n0, xs) - a = sfs.util.source_selection_point(n0, x0, xs) - plot(d, a) + d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_point( + omega, array.x, array.n, xs) + plot(d, selection, secondary_source) """ x0 = util.asarray_of_rows(x0) @@ -125,10 +130,12 @@ def wfs_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): k = util.wavenumber(omega, c) ds = x0 - xs r = np.linalg.norm(ds, axis=1) - - return wfs_25d_preeq(omega, omalias, c) * \ - np.sqrt(np.linalg.norm(xref - x0)) * inner1d(ds, n0) / \ - r ** (3 / 2) * np.exp(-1j * k * r) + d = ( + wfs_25d_preeq(omega, omalias, c) * + np.sqrt(np.linalg.norm(xref - x0)) * inner1d(ds, n0) / + r ** (3 / 2) * np.exp(-1j * k * r)) + selection = util.source_selection_point(n0, x0, xs) + return d, selection, secondary_source_point(omega, c) wfs_3d_point = _wfs_point @@ -149,16 +156,18 @@ def _wfs_plane(omega, x0, n0, n=[0, 1, 0], c=None): .. plot:: :context: close-figs - d = sfs.mono.drivingfunction.wfs_3d_plane(omega, x0, n0, npw) - a = sfs.util.source_selection_plane(n0, npw) - plot(d, a) + d, selection, secondary_source = sfs.mono.drivingfunction.wfs_3d_plane( + omega, array.x, array.n, npw) + plot(d, selection, secondary_source) """ x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) n = util.normalize_vector(n) k = util.wavenumber(omega, c) - return 2j * k * np.inner(n, n0) * np.exp(-1j * k * np.inner(n, x0)) + d = 2j * k * np.inner(n, n0) * np.exp(-1j * k * np.inner(n, x0)) + selection = util.source_selection_plane(n0, n) + return d, selection, secondary_source_point(omega, c) wfs_2d_plane = _wfs_plane @@ -179,9 +188,9 @@ def wfs_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, .. plot:: :context: close-figs - d = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw) - a = sfs.util.source_selection_plane(n0, npw) - plot(d, a) + d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_plane( + omega, array.x, array.n, npw) + plot(d, selection, secondary_source) """ x0 = util.asarray_of_rows(x0) @@ -189,15 +198,18 @@ def wfs_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, n = util.normalize_vector(n) xref = util.asarray_1d(xref) k = util.wavenumber(omega, c) - return wfs_25d_preeq(omega, omalias, c) * \ - np.sqrt(8*np.pi * np.linalg.norm(xref - x0, axis=-1)) * \ - np.inner(n, n0) * np.exp(-1j * k * np.inner(n, x0)) + d = ( + wfs_25d_preeq(omega, omalias, c) * + np.sqrt(8*np.pi * np.linalg.norm(xref - x0, axis=-1)) * + np.inner(n, n0) * np.exp(-1j * k * np.inner(n, x0))) + selection = util.source_selection_plane(n0, n) + return d, selection, secondary_source_point(omega, c) wfs_3d_plane = _wfs_plane -def _wfs_focused(omega, x0, n0, xs, c=None): +def _wfs_focused(omega, x0, n0, xs, ns, c=None): r"""Focused source by two- or three-dimensional WFS. .. math:: @@ -211,9 +223,9 @@ def _wfs_focused(omega, x0, n0, xs, c=None): .. plot:: :context: close-figs - d = sfs.mono.drivingfunction.wfs_3d_focused(omega, x0, n0, xs_focused) - a = sfs.util.source_selection_focused(ns, x0, xs_focused) - plot(d, a) + d, selection, secondary_source = sfs.mono.drivingfunction.wfs_3d_focused( + omega, array.x, array.n, xs_focused, ns_focused) + plot(d, selection, secondary_source) """ x0 = util.asarray_of_rows(x0) @@ -222,13 +234,16 @@ def _wfs_focused(omega, x0, n0, xs, c=None): k = util.wavenumber(omega, c) ds = x0 - xs r = np.linalg.norm(ds, axis=1) - return 1j * k * inner1d(ds, n0) / r ** (3 / 2) * np.exp(1j * k * r) + d = 1j * k * inner1d(ds, n0) / r ** (3 / 2) * np.exp(1j * k * r) + selection = util.source_selection_focused(ns, x0, xs) + return d, selection, secondary_source_point(omega, c) wfs_2d_focused = _wfs_focused -def wfs_25d_focused(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): +def wfs_25d_focused(omega, x0, n0, xs, ns, xref=[0, 0, 0], c=None, + omalias=None): r"""Focused source by 2.5-dimensional WFS. .. math:: @@ -243,9 +258,9 @@ def wfs_25d_focused(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): .. plot:: :context: close-figs - d = sfs.mono.drivingfunction.wfs_25d_focused(omega, x0, n0, xs_focused) - a = sfs.util.source_selection_focused(ns, x0, xs_focused) - plot(d, a) + d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_focused( + omega, array.x, array.n, xs_focused, ns_focused) + plot(d, selection, secondary_source) """ x0 = util.asarray_of_rows(x0) @@ -255,10 +270,12 @@ def wfs_25d_focused(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): k = util.wavenumber(omega, c) ds = x0 - xs r = np.linalg.norm(ds, axis=1) - - return wfs_25d_preeq(omega, omalias, c) * \ - np.sqrt(np.linalg.norm(xref - x0)) * inner1d(ds, n0) / \ - r ** (3 / 2) * np.exp(1j * k * r) + d = ( + wfs_25d_preeq(omega, omalias, c) * + np.sqrt(np.linalg.norm(xref - x0)) * inner1d(ds, n0) / + r ** (3 / 2) * np.exp(1j * k * r)) + selection = util.source_selection_focused(ns, x0, xs) + return d, selection, secondary_source_point(omega, c) wfs_3d_focused = _wfs_focused @@ -280,7 +297,9 @@ def delay_3d_plane(omega, x0, n0, n=[0, 1, 0], c=None): x0 = util.asarray_of_rows(x0) n = util.normalize_vector(n) k = util.wavenumber(omega, c) - return np.exp(-1j * k * np.inner(n, x0)) + d = np.exp(-1j * k * np.inner(n, x0)) + selection = util.source_selection_plane(n0, n) + return d, selection, secondary_source_point(omega, c) def nfchoa_2d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): @@ -301,8 +320,9 @@ def nfchoa_2d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): .. plot:: :context: close-figs - d = sfs.mono.drivingfunction.nfchoa_2d_plane(omega, x0, R, npw) - plot(d, 1) + d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_2d_plane( + omega, array.x, R, npw) + plot(d, selection, secondary_source) """ x0 = util.asarray_of_rows(x0) @@ -314,7 +334,8 @@ def nfchoa_2d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): d = 0 for m in range(-M, M + 1): d += 1j**-m / hankel2(m, k * r0) * np.exp(1j * m * (phi0 - phi)) - return -2j / (np.pi*r0) * d + selection = util.source_selection_all(len(x0)) + return -2j / (np.pi*r0) * d, selection, secondary_source_point(omega, c) def nfchoa_25d_point(omega, x0, r0, xs, max_order=None, c=None): @@ -335,8 +356,9 @@ def nfchoa_25d_point(omega, x0, r0, xs, max_order=None, c=None): .. plot:: :context: close-figs - d = sfs.mono.drivingfunction.nfchoa_25d_point(omega, x0, R, xs) - plot(d, 1) + d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_25d_point( + omega, array.x, R, xs) + plot(d, selection, secondary_source) """ x0 = util.asarray_of_rows(x0) @@ -350,7 +372,8 @@ def nfchoa_25d_point(omega, x0, r0, xs, max_order=None, c=None): d = 0 for m in range(-M, M + 1): d += hr[abs(m)] / hr0[abs(m)] * np.exp(1j * m * (phi0 - phi)) - return d / (2 * np.pi * r0) + selection = util.source_selection_all(len(x0)) + return d / (2 * np.pi * r0), selection, secondary_source_point(omega, c) def nfchoa_25d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): @@ -371,8 +394,9 @@ def nfchoa_25d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): .. plot:: :context: close-figs - d = sfs.mono.drivingfunction.nfchoa_25d_plane(omega, x0, R, npw) - plot(d, 1) + d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_25d_plane( + omega, array.x, R, npw) + plot(d, selection, secondary_source) """ x0 = util.asarray_of_rows(x0) @@ -385,7 +409,8 @@ def nfchoa_25d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): hn2 = util.spherical_hn2(range(0, M + 1), k * r0) for m in range(-M, M + 1): d += (-1j)**abs(m) / (k * hn2[abs(m)]) * np.exp(1j * m * (phi0 - phi)) - return 2*1j / r0 * d + selection = util.source_selection_all(len(x0)) + return 2*1j / r0 * d, selection, secondary_source_point(omega, c) def sdm_2d_line(omega, x0, n0, xs, c=None): @@ -741,6 +766,24 @@ def esa_edge_dipole_2d_line(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): return -1j*np.pi/alpha * d +def secondary_source_point(omega, c): + """Create a point source for use in `sfs.mono.synthesize()`.""" + + def secondary_source(position, _, grid): + return _source.point(omega, position, grid, c) + + return secondary_source + + +def secondary_source_line(omega, c): + """Create a line source for use in `sfs.mono.synthesize()`.""" + + def secondary_source(position, _, grid): + return _source.line(omega, position, grid, c) + + return secondary_source + + def _max_order_circular_harmonics(N, max_order): """Compute order of 2D HOA.""" return N // 2 if max_order is None else max_order diff --git a/sfs/mono/soundfigure.py b/sfs/mono/soundfigure.py index 8167c9d..fcaf4f5 100644 --- a/sfs/mono/soundfigure.py +++ b/sfs/mono/soundfigure.py @@ -2,7 +2,7 @@ import numpy as np from .. import util -from . import drivingfunction +from .drivingfunction import wfs_3d_plane def wfs_3d_pw(omega, x0, n0, figure, npw=[0, 0, 1], c=None): @@ -39,8 +39,8 @@ def wfs_3d_pw(omega, x0, n0, figure, npw=[0, 0, 1], c=None): npw = 1/k * np.asarray([kx[n], ky[m], kz]) npw = npw / np.linalg.norm(npw) # driving function of plane wave with positive kz - a = util.source_selection_plane(n0, npw) - a = a * figure[n, m] - d += a * drivingfunction.wfs_3d_plane(omega, x0, n0, npw, c) + d_component, selection, secondary_source = wfs_3d_plane( + omega, x0, n0, npw, c) + d += selection * figure[n, m] * d_component - return d + return d, util.source_selection_all(len(d)), secondary_source diff --git a/sfs/mono/source.py b/sfs/mono/source.py index 7fc408d..05ad6cd 100644 --- a/sfs/mono/source.py +++ b/sfs/mono/source.py @@ -32,7 +32,7 @@ from .. import default -def point(omega, x0, n0, grid, c=None): +def point(omega, x0, grid, c=None): r"""Sound pressure of a point source. Parameters @@ -41,8 +41,6 @@ def point(omega, x0, n0, grid, c=None): Frequency of source. x0 : (3,) array_like Position of source. - n0 : (3,) array_like - Normal vector (direction) of source. Only for compatibilty, not used. grid : triple of array_like The grid that is used for the sound field calculations. See `sfs.util.xyz_grid()`. @@ -65,7 +63,7 @@ def point(omega, x0, n0, grid, c=None): .. plot:: :context: close-figs - p = sfs.mono.source.point(omega, x0, None, grid) + p = sfs.mono.source.point(omega, x0, grid) sfs.plot.soundfield(p, grid) plt.title("Point Source at {} m".format(x0)) @@ -87,7 +85,7 @@ def point(omega, x0, n0, grid, c=None): return 1 / (4*np.pi) * np.exp(-1j * k * r) / r -def point_velocity(omega, x0, n0, grid, c=None, rho0=None): +def point_velocity(omega, x0, grid, c=None, rho0=None): """Particle velocity of a point source. Parameters @@ -96,8 +94,6 @@ def point_velocity(omega, x0, n0, grid, c=None, rho0=None): Frequency of source. x0 : (3,) array_like Position of source. - n0 : (3,) array_like - Normal vector (direction) of source. Only for compatibilty, not used. grid : triple of array_like The grid that is used for the sound field calculations. See `sfs.util.xyz_grid()`. @@ -118,7 +114,7 @@ def point_velocity(omega, x0, n0, grid, c=None, rho0=None): .. plot:: :context: close-figs - v = sfs.mono.source.point_velocity(omega, x0, None, vgrid) + v = sfs.mono.source.point_velocity(omega, x0, vgrid) sfs.plot.soundfield(p * normalization_point, grid) sfs.plot.vectors(v * normalization_point, vgrid) plt.title("Sound Pressure and Particle Velocity") @@ -133,12 +129,12 @@ def point_velocity(omega, x0, n0, grid, c=None, rho0=None): grid = util.as_xyz_components(grid) offset = grid - x0 r = np.linalg.norm(offset) - v = point(omega, x0, n0, grid, c=c) + v = point(omega, x0, grid, c=c) v *= (1+1j*k*r) / (rho0 * c * 1j*k*r) return util.XyzComponents([v * o / r for o in offset]) -def point_averaged_intensity(omega, x0, n0, grid, c=None, rho0=None): +def point_averaged_intensity(omega, x0, grid, c=None, rho0=None): """Velocity of a point source. Parameters @@ -147,8 +143,6 @@ def point_averaged_intensity(omega, x0, n0, grid, c=None, rho0=None): Frequency of source. x0 : (3,) array_like Position of source. - n0 : (3,) array_like - Normal vector (direction) of source. Only for compatibilty, not used. grid : triple of array_like The grid that is used for the sound field calculations. See `sfs.util.xyz_grid()`. @@ -227,7 +221,7 @@ def point_dipole(omega, x0, n0, grid, c=None): np.power(r, 2) * np.exp(-1j * k * r) -def point_modal(omega, x0, n0, grid, L, N=None, deltan=0, c=None): +def point_modal(omega, x0, grid, L, N=None, deltan=0, c=None): """Point source in a rectangular room using a modal room model. Parameters @@ -236,9 +230,6 @@ def point_modal(omega, x0, n0, grid, L, N=None, deltan=0, c=None): Frequency of source. x0 : (3,) array_like Position of source. - n0 : (3,) array_like - Normal vector (direction) of source (only required for - compatibility). grid : triple of array_like The grid that is used for the sound field calculations. See `sfs.util.xyz_grid()`. @@ -295,7 +286,7 @@ def point_modal(omega, x0, n0, grid, L, N=None, deltan=0, c=None): return p -def point_modal_velocity(omega, x0, n0, grid, L, N=None, deltan=0, c=None): +def point_modal_velocity(omega, x0, grid, L, N=None, deltan=0, c=None): """Velocity of point source in a rectangular room using a modal room model. Parameters @@ -304,9 +295,6 @@ def point_modal_velocity(omega, x0, n0, grid, L, N=None, deltan=0, c=None): Frequency of source. x0 : (3,) array_like Position of source. - n0 : (3,) array_like - Normal vector (direction) of source (only required for - compatibility). grid : triple of array_like The grid that is used for the sound field calculations. See `sfs.util.xyz_grid()`. @@ -370,8 +358,7 @@ def point_modal_velocity(omega, x0, n0, grid, L, N=None, deltan=0, c=None): return util.XyzComponents([vx, vy, vz]) -def point_image_sources(omega, x0, n0, grid, L, max_order, coeffs=None, - c=None): +def point_image_sources(omega, x0, grid, L, max_order, coeffs=None, c=None): """Point source in a rectangular room using the mirror image source model. Parameters @@ -380,9 +367,6 @@ def point_image_sources(omega, x0, n0, grid, L, max_order, coeffs=None, Frequency of source. x0 : (3,) array_like Position of source. - n0 : (3,) array_like - Normal vector (direction) of source (only required for - compatibility). grid : triple of array_like The grid that is used for the sound field calculations. See `sfs.util.xyz_grid()`. @@ -411,12 +395,12 @@ def point_image_sources(omega, x0, n0, grid, L, max_order, coeffs=None, p = 0 for position, strength in zip(xs, source_strengths): if strength != 0: - p += strength * point(omega, position, n0, grid, c) + p += strength * point(omega, position, grid, c) return p -def line(omega, x0, n0, grid, c=None): +def line(omega, x0, grid, c=None): r"""Line source parallel to the z-axis. Note: third component of x0 is ignored. @@ -432,7 +416,7 @@ def line(omega, x0, n0, grid, c=None): .. plot:: :context: close-figs - p = sfs.mono.source.line(omega, x0, None, grid) + p = sfs.mono.source.line(omega, x0, grid) sfs.plot.soundfield(p, grid) plt.title("Line Source at {} m".format(x0[:2])) @@ -455,7 +439,7 @@ def line(omega, x0, n0, grid, c=None): return _duplicate_zdirection(p, grid) -def line_velocity(omega, x0, n0, grid, c=None, rho0=None): +def line_velocity(omega, x0, grid, c=None, rho0=None): """Velocity of line source parallel to the z-axis. Returns @@ -470,7 +454,7 @@ def line_velocity(omega, x0, n0, grid, c=None, rho0=None): .. plot:: :context: close-figs - v = sfs.mono.source.line_velocity(omega, x0, None, vgrid) + v = sfs.mono.source.line_velocity(omega, x0, vgrid) sfs.plot.soundfield(p * normalization_line, grid) sfs.plot.vectors(v * normalization_line, vgrid) plt.title("Sound Pressure and Particle Velocity") diff --git a/sfs/mono/synthesized.py b/sfs/mono/synthesized.py deleted file mode 100644 index 935562b..0000000 --- a/sfs/mono/synthesized.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Computation of synthesized sound fields.""" - -import numpy as np -from .source import point - - -def generic(omega, x0, n0, d, grid, c=None, source=point): - """Compute sound field for a generic driving function.""" - d = np.squeeze(np.asarray(d)) - if len(d) != len(x0): - raise ValueError("length mismatch") - p = 0 - for weight, position, direction in zip(d, x0, n0): - if weight != 0: - p += weight * source(omega, position, direction, grid, c) - return p - - -def shiftphase(p, phase): - """Shift phase of a sound field.""" - p = np.asarray(p) - return p * np.exp(1j * phase) diff --git a/sfs/time/__init__.py b/sfs/time/__init__.py index cd7430b..48cc2cc 100644 --- a/sfs/time/__init__.py +++ b/sfs/time/__init__.py @@ -5,9 +5,61 @@ drivingfunction source - soundfield """ from . import drivingfunction from . import source -from . import soundfield + +from .. import util as _util +from .. import array as _array + + +def synthesize(signals, weights, ssd, secondary_source_function, **kwargs): + """Compute sound field for an array of secondary sources. + + Parameters + ---------- + signals : (N, C) array_like + float + Driving signals consisting of audio data (C channels) and a + sampling rate (in Hertz). + A `DelayedSignal` object can also be used. + weights : (C,) array_like + Additional weights applied during integration, e.g. source + selection and tapering. + ssd : sequence of between 1 and 3 array_like objects + Positions (shape ``(C, 3)``), normal vectors (shape ``(C, 3)``) + and weights (shape ``(C,)``) of secondary sources. + A `SecondarySourceDistribution` can also be used. + secondary_source_function : callable + A function that generates the sound field of a secondary source. + This signature is expected:: + + secondary_source_function( + position, normal_vector, weight, driving_signal, + **kwargs) -> numpy.ndarray + + **kwargs + All keyword arguments are forwarded to *secondary_source_function*. + This is typically used to pass the *observation_time* and *grid* + arguments. + + Returns + ------- + numpy.ndarray + Sound pressure at grid positions. + + """ + ssd = _array.as_secondary_source_distribution(ssd) + data, samplerate, signal_offset = _util.as_delayed_signal(signals) + weights = _util.asarray_1d(weights) + channels = data.T + if not (len(ssd.x) == len(ssd.n) == len(ssd.a) == len(channels) == + len(weights)): + raise ValueError("Length mismatch") + p = 0 + for x, n, a, channel, weight in zip(ssd.x, ssd.n, ssd.a, + channels, weights): + if weight != 0: + signal = channel, samplerate, signal_offset + p += a * weight * secondary_source_function(x, n, signal, **kwargs) + return p diff --git a/sfs/time/drivingfunction.py b/sfs/time/drivingfunction.py index 55bddf3..40d0a77 100644 --- a/sfs/time/drivingfunction.py +++ b/sfs/time/drivingfunction.py @@ -30,20 +30,22 @@ # Circular loudspeaker array N = 32 # number of loudspeakers R = 1.5 # radius - x0, n0, a0 = sfs.array.circular(N, R) + array = sfs.array.circular(N, R) grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02) - def plot(d, selected, t=0): - p = sfs.time.soundfield.p_array(x0, d, selected * a0, t, grid) + def plot(d, selection, secondary_source, t=0): + p = sfs.time.synthesize(d, selection, array, secondary_source, grid=grid, + observation_time=t) sfs.plot.level(p, grid) - sfs.plot.loudspeaker_2d(x0, n0, selected * a0, size=0.15) + sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) """ import numpy as np from numpy.core.umath_tests import inner1d # element-wise inner product from .. import default from .. import util +from . import source as _source def wfs_25d_plane(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): @@ -66,8 +68,14 @@ def wfs_25d_plane(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): ------- delays : (N,) numpy.ndarray Delays of secondary sources in seconds. - weights: (N,) numpy.ndarray + weights : (N,) numpy.ndarray Weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.time.synthesize()`. Notes ----- @@ -96,10 +104,10 @@ def wfs_25d_plane(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): .. plot:: :context: close-figs - delays, weights = sfs.time.drivingfunction.wfs_25d_plane(x0, n0, npw) + delays, weights, selection, secondary_source = \ + sfs.time.drivingfunction.wfs_25d_plane(array.x, array.n, npw) d = sfs.time.drivingfunction.driving_signals(delays, weights, signal) - a = sfs.util.source_selection_plane(n0, npw) - plot(d, a) + plot(d, selection, secondary_source) """ if c is None: @@ -111,7 +119,8 @@ def wfs_25d_plane(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): g0 = np.sqrt(2 * np.pi * np.linalg.norm(xref - x0, axis=1)) delays = inner1d(n, x0) / c weights = 2 * g0 * inner1d(n, n0) - return delays, weights + selection = util.source_selection_plane(n0, n) + return delays, weights, selection, secondary_source_point(c) def wfs_25d_point(x0, n0, xs, xref=[0, 0, 0], c=None): @@ -136,6 +145,12 @@ def wfs_25d_point(x0, n0, xs, xref=[0, 0, 0], c=None): Delays of secondary sources in seconds. weights: (N,) numpy.ndarray Weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.time.synthesize()`. Notes ----- @@ -166,10 +181,10 @@ def wfs_25d_point(x0, n0, xs, xref=[0, 0, 0], c=None): .. plot:: :context: close-figs - delays, weights = sfs.time.drivingfunction.wfs_25d_point(x0, n0, xs) + delays, weights, selection, secondary_source = \ + sfs.time.drivingfunction.wfs_25d_point(array.x, array.n, xs) d = sfs.time.drivingfunction.driving_signals(delays, weights, signal) - a = sfs.util.source_selection_point(n0, x0, xs) - plot(d, a, t=ts) + plot(d, selection, secondary_source, t=ts) """ if c is None: @@ -183,10 +198,11 @@ def wfs_25d_point(x0, n0, xs, xref=[0, 0, 0], c=None): r = np.linalg.norm(ds, axis=1) delays = r/c weights = g0 * inner1d(ds, n0) / (2 * np.pi * r**(3/2)) - return delays, weights + selection = util.source_selection_point(n0, x0, xs) + return delays, weights, selection, secondary_source_point(c) -def wfs_25d_focused(x0, n0, xs, xref=[0, 0, 0], c=None): +def wfs_25d_focused(x0, n0, xs, ns, xref=[0, 0, 0], c=None): r"""Point source by 2.5-dimensional WFS. Parameters @@ -197,6 +213,10 @@ def wfs_25d_focused(x0, n0, xs, xref=[0, 0, 0], c=None): Sequence of secondary source orientations. xs : (3,) array_like Virtual source position. + ns : (3,) array_like + Normal vector (propagation direction) of focused source. + This is used for secondary source selection, + see `sfs.util.source_selection_focused()`. xref : (3,) array_like, optional Reference position c : float, optional @@ -208,6 +228,12 @@ def wfs_25d_focused(x0, n0, xs, xref=[0, 0, 0], c=None): Delays of secondary sources in seconds. weights: (N,) numpy.ndarray Weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.time.synthesize()`. Notes ----- @@ -239,10 +265,10 @@ def wfs_25d_focused(x0, n0, xs, xref=[0, 0, 0], c=None): .. plot:: :context: close-figs - delays, weights = sfs.time.drivingfunction.wfs_25d_focused(x0, n0, xf) + delays, weights, selection, secondary_source = \ + sfs.time.drivingfunction.wfs_25d_focused(array.x, array.n, xf, nf) d = sfs.time.drivingfunction.driving_signals(delays, weights, signal) - a = sfs.util.source_selection_focused(nf, x0, xf) - plot(d, a, t=tf) + plot(d, selection, secondary_source, t=tf) """ if c is None: @@ -257,7 +283,8 @@ def wfs_25d_focused(x0, n0, xs, xref=[0, 0, 0], c=None): / (np.linalg.norm(xref - x0, axis=1) + r)) delays = -r/c weights = g0 * inner1d(ds, n0) / (2 * np.pi * r**(3/2)) - return delays, weights + selection = util.source_selection_focused(ns, x0, xs) + return delays, weights, selection, secondary_source_point(c) def driving_signals(delays, weights, signal): @@ -321,3 +348,12 @@ def apply_delays(signal, delays): for column, row in enumerate(delays_samples): out[row:row + len(data), column] = data return util.DelayedSignal(out, samplerate, offset_samples / samplerate) + + +def secondary_source_point(c): + """Create a point source for use in `sfs.time.synthesize()`.""" + + def secondary_source(position, _, signal, observation_time, grid): + return _source.point(position, signal, observation_time, grid, c=c) + + return secondary_source diff --git a/sfs/time/soundfield.py b/sfs/time/soundfield.py deleted file mode 100644 index 7bbb05d..0000000 --- a/sfs/time/soundfield.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Compute sound field.""" - -from .. import util -from .. import default -from .source import point - - -def p_array(x0, signals, weights, observation_time, grid, source=point, - c=None): - """Compute sound field for an array of secondary sources. - - Parameters - ---------- - x0 : (N, 3) array_like - Sequence of secondary source positions. - signals : (N, C) array_like + float - Driving signals consisting of audio data (C channels) and a - sampling rate (in Hertz). - A `DelayedSignal` object can also be used. - weights : (C,) array_like - Additional weights applied during integration, e.g. source - tapering. - observation_time : float - Simulation point in time (seconds). - grid : triple of array_like - The grid that is used for the sound field calculations. - See `sfs.util.xyz_grid()`. - source: function, optional - Source type is a function, returning scalar field. - For default, see `sfs.time.source.point()`. - c : float, optional - Speed of sound. - - Returns - ------- - numpy.ndarray - Sound pressure at grid positions. - - """ - if c is None: - c = default.c - x0 = util.asarray_of_rows(x0) - data, samplerate, signal_offset = util.as_delayed_signal(signals) - weights = util.asarray_1d(weights) - channels = data.T - if not (len(weights) == len(x0) == len(channels)): - raise ValueError("Length mismatch") - # synthesize soundfield - p = 0 - for channel, weight, position in zip(channels, weights, x0): - if weight != 0: - signal = channel, samplerate, signal_offset - p_s = source(position, signal, observation_time, grid, c) - p += p_s * weight # integrate over secondary sources - return p diff --git a/tests/test_array.py b/tests/test_array.py index c5f05fd..5100f41 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -21,17 +21,17 @@ def vector_id(vector): @pytest.mark.parametrize('N, spacing, result', [ - (2, 1, sfs.array.ArrayData( + (2, 1, sfs.array.SecondarySourceDistribution( x=[[0, -0.5, 0], [0, 0.5, 0]], n=[[1, 0, 0], [1, 0, 0]], a=[1, 1], )), - (3, 1, sfs.array.ArrayData( + (3, 1, sfs.array.SecondarySourceDistribution( x=[[0, -1, 0], [0, 0, 0], [0, 1, 0]], n=[[1, 0, 0], [1, 0, 0], [1, 0, 0]], a=[1, 1, 1], )), - (3, 0.5, sfs.array.ArrayData( + (3, 0.5, sfs.array.SecondarySourceDistribution( x=[[0, -0.5, 0], [0, 0, 0], [0, 0.5, 0]], n=[[1, 0, 0], [1, 0, 0], [1, 0, 0]], a=[0.5, 0.5, 0.5], From bc510780ee3d2a3a65f6d37a4e97cedf98a1a1d8 Mon Sep 17 00:00:00 2001 From: Chris Hold Date: Fri, 8 Mar 2019 12:55:34 +0100 Subject: [PATCH 038/101] DOCS: Update cart2sph range --- sfs/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sfs/util.py b/sfs/util.py index 61e3f0e..b00e0ce 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -102,7 +102,7 @@ def cart2sph(x, y, z): \beta = \arccos \left( \frac{z}{r} \right) \\ r = \sqrt{x^2 + y^2 + z^2} - with :math:`\alpha \in [0, 2\pi), \beta \in [0, \pi], r \geq 0` + with :math:`\alpha \in [-pi, pi], \beta \in [0, \pi], r \geq 0` Parameters ---------- From 1c7f25099f33a840cd3bc34668b9d72ecbfed0e8 Mon Sep 17 00:00:00 2001 From: Chris Hold Date: Fri, 8 Mar 2019 12:59:16 +0100 Subject: [PATCH 039/101] DOCS: Rename Elevation to Colatitude --- sfs/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sfs/util.py b/sfs/util.py index b00e0ce..8f02ef7 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -72,7 +72,7 @@ def sph2cart(alpha, beta, r): alpha : float or array_like Azimuth angle in radiants beta : float or array_like - Elevation angle in radiants (with 0 denoting North pole) + Colatitude angle in radiants (with 0 denoting North pole) r : float or array_like Radius @@ -118,7 +118,7 @@ def cart2sph(x, y, z): alpha : float or array_like Azimuth angle in radiants beta : float or array_like - Elevation angle in radiants (with 0 denoting North pole) + Colatitude angle in radiants (with 0 denoting North pole) r : float or array_like Radius From b6eb6e04ccaab22f65075a50b028952cd26b93ea Mon Sep 17 00:00:00 2001 From: Chris Hold Date: Fri, 8 Mar 2019 13:05:17 +0100 Subject: [PATCH 040/101] Test cart2sph against pre-defined values --- tests/test_util.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_util.py b/tests/test_util.py index d46fd76..6eb7a92 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -3,16 +3,16 @@ import pytest import sfs + cart_sph_data = [ ((1, 1, 1), (np.pi / 4, np.arccos(1 / np.sqrt(3)), np.sqrt(3))), - ((-1, 1, 1), (np.arctan2(1, -1), np.arccos(1 / np.sqrt(3)), np.sqrt(3))), + ((-1, 1, 1), (3 / 4 * np.pi, np.arccos(1 / np.sqrt(3)), np.sqrt(3))), ((1, -1, 1), (-np.pi / 4, np.arccos(1 / np.sqrt(3)), np.sqrt(3))), - ((-1, -1, 1), (np.arctan2(-1, -1), np.arccos(1 / np.sqrt(3)), np.sqrt(3))), + ((-1, -1, 1), (-3 / 4 * np.pi, np.arccos(1 / np.sqrt(3)), np.sqrt(3))), ((1, 1, -1), (np.pi / 4, np.arccos(-1 / np.sqrt(3)), np.sqrt(3))), - ((-1, 1, -1), (np.arctan2(1, -1), np.arccos(-1 / np.sqrt(3)), np.sqrt(3))), + ((-1, 1, -1), (3 / 4 * np.pi, np.arccos(-1 / np.sqrt(3)), np.sqrt(3))), ((1, -1, -1), (-np.pi / 4, np.arccos(-1 / np.sqrt(3)), np.sqrt(3))), - ((-1, -1, -1), (np.arctan2(-1, -1), - np.arccos(-1 / np.sqrt(3)), np.sqrt(3))), + ((-1, -1, -1), (-3 / 4 * np.pi, np.arccos(-1 / np.sqrt(3)), np.sqrt(3))), ] From 4395c15f640096f73cf4b739f076c535fda04c40 Mon Sep 17 00:00:00 2001 From: Nara Hahn Date: Sat, 7 Jan 2017 01:34:55 +0100 Subject: [PATCH 041/101] Time-domain NFC-HOA --- doc/examples/time_domain_nfchoa.py | 50 ++++ sfs/mono/drivingfunction.py | 29 +- sfs/time/drivingfunction.py | 452 ++++++++++++++++++++++++++++- sfs/time/source.py | 3 +- sfs/util.py | 42 +++ 5 files changed, 559 insertions(+), 17 deletions(-) create mode 100644 doc/examples/time_domain_nfchoa.py diff --git a/doc/examples/time_domain_nfchoa.py b/doc/examples/time_domain_nfchoa.py new file mode 100644 index 0000000..6195b99 --- /dev/null +++ b/doc/examples/time_domain_nfchoa.py @@ -0,0 +1,50 @@ +"""Create some examples of time-domain NFC-HOA.""" + +import numpy as np +import matplotlib.pyplot as plt +import sfs +from scipy.signal import unit_impulse + +# Parameters +fs = 44100 # sampling frequency +grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.005) +N = 60 # number of secondary sources +R = 1.5 # radius of circular array +array = sfs.array.circular(N, R) + +# Excitation signal +signal = unit_impulse(512), fs, 0 + +# Plane wave +max_order = None +npw = [0, -1, 0] # propagating direction +t = 0 # observation time +delay, weight, sos, phaseshift, selection, secondary_source = \ + sfs.time.drivingfunction.nfchoa_25d_plane(array.x, R, npw, fs, max_order) +d = sfs.time.drivingfunction.nfchoa_25d_driving_signals( + delay, weight, sos, phaseshift, signal) +p = sfs.time.synthesize(d, selection, array, secondary_source, + observation_time=t, grid=grid) + +plt.figure() +sfs.plot.level(p, grid) +sfs.plot.loudspeaker_2d(array.x, array.n) +sfs.plot.virtualsource_2d([0, 0], ns=npw, type='plane') +plt.savefig('impulse_pw_nfchoa_25d.png') + +# Point source +max_order = 100 +xs = [1.5, 1.5, 0] # position +t = np.linalg.norm(xs) / sfs.default.c # observation time +delay, weight, sos, phaseshift, selection, secondary_source = \ + sfs.time.drivingfunction.nfchoa_25d_point(array.x, R, xs, fs, max_order) +d = sfs.time.drivingfunction.nfchoa_25d_driving_signals( + delay, weight, sos, phaseshift, signal) +p = sfs.time.synthesize(d, selection, array, secondary_source, + observation_time=t, grid=grid) + +plt.figure() +sfs.plot.level(p, grid) +sfs.plot.loudspeaker_2d(array.x, array.n) +sfs.plot.virtualsource_2d(xs, type='point') +plt.savefig('impulse_ps_nfchoa_25d.png') diff --git a/sfs/mono/drivingfunction.py b/sfs/mono/drivingfunction.py index fd188ca..cd08f77 100644 --- a/sfs/mono/drivingfunction.py +++ b/sfs/mono/drivingfunction.py @@ -325,14 +325,16 @@ def nfchoa_2d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): plot(d, selection, secondary_source) """ + if max_order is None: + max_order = util.max_order_circular_harmonics(len(x0)) + x0 = util.asarray_of_rows(x0) k = util.wavenumber(omega, c) n = util.normalize_vector(n) phi, _, r = util.cart2sph(*n) phi0 = util.cart2sph(*x0.T)[0] - M = _max_order_circular_harmonics(len(x0), max_order) d = 0 - for m in range(-M, M + 1): + for m in range(-max_order, max_order + 1): d += 1j**-m / hankel2(m, k * r0) * np.exp(1j * m * (phi0 - phi)) selection = util.source_selection_all(len(x0)) return -2j / (np.pi*r0) * d, selection, secondary_source_point(omega, c) @@ -361,16 +363,18 @@ def nfchoa_25d_point(omega, x0, r0, xs, max_order=None, c=None): plot(d, selection, secondary_source) """ + if max_order is None: + max_order = util.max_order_circular_harmonics(len(x0)) + x0 = util.asarray_of_rows(x0) k = util.wavenumber(omega, c) xs = util.asarray_1d(xs) phi, _, r = util.cart2sph(*xs) phi0 = util.cart2sph(*x0.T)[0] - M = _max_order_circular_harmonics(len(x0), max_order) - hr = util.spherical_hn2(range(0, M + 1), k * r) - hr0 = util.spherical_hn2(range(0, M + 1), k * r0) + hr = util.spherical_hn2(range(0, max_order + 1), k * r) + hr0 = util.spherical_hn2(range(0, max_order + 1), k * r0) d = 0 - for m in range(-M, M + 1): + for m in range(-max_order, max_order + 1): d += hr[abs(m)] / hr0[abs(m)] * np.exp(1j * m * (phi0 - phi)) selection = util.source_selection_all(len(x0)) return d / (2 * np.pi * r0), selection, secondary_source_point(omega, c) @@ -399,15 +403,17 @@ def nfchoa_25d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): plot(d, selection, secondary_source) """ + if max_order is None: + max_order = util.max_order_circular_harmonics(len(x0)) + x0 = util.asarray_of_rows(x0) k = util.wavenumber(omega, c) n = util.normalize_vector(n) phi, _, r = util.cart2sph(*n) phi0 = util.cart2sph(*x0.T)[0] - M = _max_order_circular_harmonics(len(x0), max_order) d = 0 - hn2 = util.spherical_hn2(range(0, M + 1), k * r0) - for m in range(-M, M + 1): + hn2 = util.spherical_hn2(range(0, max_order + 1), k * r0) + for m in range(-max_order, max_order + 1): d += (-1j)**abs(m) / (k * hn2[abs(m)]) * np.exp(1j * m * (phi0 - phi)) selection = util.source_selection_all(len(x0)) return 2*1j / r0 * d, selection, secondary_source_point(omega, c) @@ -782,8 +788,3 @@ def secondary_source(position, _, grid): return _source.line(omega, position, grid, c) return secondary_source - - -def _max_order_circular_harmonics(N, max_order): - """Compute order of 2D HOA.""" - return N // 2 if max_order is None else max_order diff --git a/sfs/time/drivingfunction.py b/sfs/time/drivingfunction.py index 40d0a77..2c942d5 100644 --- a/sfs/time/drivingfunction.py +++ b/sfs/time/drivingfunction.py @@ -7,8 +7,8 @@ import matplotlib.pyplot as plt import numpy as np - from scipy.signal import unit_impulse import sfs + from scipy.signal import unit_impulse # Plane wave npw = sfs.util.direction_vector(np.radians(-45)) @@ -25,7 +25,8 @@ tf = rf / sfs.default.c # time-of-arrival at origin # Impulsive excitation - signal = unit_impulse(512), 44100 + fs = 44100 + signal = unit_impulse(512), fs # Circular loudspeaker array N = 32 # number of loudspeakers @@ -43,6 +44,8 @@ def plot(d, selection, secondary_source, t=0): """ import numpy as np from numpy.core.umath_tests import inner1d # element-wise inner product +from scipy.signal import besselap, sosfilt, zpk2sos +from scipy.special import eval_legendre as legendre from .. import default from .. import util from . import source as _source @@ -357,3 +360,448 @@ def secondary_source(position, _, signal, observation_time, grid): return _source.point(position, signal, observation_time, grid, c=c) return secondary_source + + +def matchedz_zpk(s_zeros, s_poles, s_gain, fs): + """Matched-z transform of poles and zeros. + + Parameters + ---------- + s_zeros : array_like + Zeros in the Laplace domain. + s_poles : array_like + Poles in the Laplace domain. + s_gain : float + System gain in the Laplace domain. + fs : int + Sampling frequency in Hertz. + + Returns + ------- + z_zeros : numpy.ndarray + Zeros in the z-domain. + z_poles : numpy.ndarray + Poles in the z-domain. + z_gain : float + System gain in the z-domain. + + See Also + -------- + :func:`scipy.signal.bilinear_zpk` + + """ + z_zeros = np.exp(s_zeros / fs) + z_poles = np.exp(s_poles / fs) + omega = 1j * np.pi * fs + s_gain *= np.prod((omega - s_zeros) / (omega - s_poles) + * (-1 - z_poles) / (-1 - z_zeros)) + return z_zeros, z_poles, np.real(s_gain) + + +def nfchoa_25d_plane(x0, r0, npw, fs, max_order=None, c=None, + s2z=matchedz_zpk): + r"""Virtual plane wave by 2.5-dimensional NFC-HOA. + + .. math:: + + D(\phi_0, s) = + 2\e{\frac{s}{c}r_0} + \sum_{m=-M}^{M} + (-1)^m + \Big(\frac{s}{s-\frac{c}{r_0}\sigma_0}\Big)^\mu + \prod_{l=1}^{\nu} + \frac{s^2}{(s-\frac{c}{r_0}\sigma_l)^2+(\frac{c}{r_0}\omega_l)^2} + \e{\i m(\phi_0 - \phi_\text{pw})} + + The driving function is represented in the Laplace domain, + from which the recursive filters are designed. + :math:`\sigma_l + \i\omega_l` denotes the complex roots of + the reverse Bessel polynomial. + The number of second-order sections is + :math:`\nu = \big\lfloor\tfrac{|m|}{2}\big\rfloor`, + whereas the number of first-order section :math:`\mu` is either 0 or 1 + for even and odd :math:`|m|`, respectively. + + Parameters + ---------- + x0 : (N, 3) array_like + Sequence of secondary source positions. + r0 : float + Radius of the circular secondary source distribution. + npw : (3,) array_like + Unit vector (propagation direction) of plane wave. + fs : int + Sampling frequency in Hertz. + max_order : int, optional + Ambisonics order. + c : float, optional + Speed of sound in m/s. + s2z : callable, optional + Function transforming s-domain poles and zeros into z-domain, + e.g. :func:`matchedz_zpk`, :func:`scipy.signal.bilinear_zpk`. + + Returns + ------- + delay : float + Overall delay in seconds. + weight : float + Overall weight. + sos : list of numpy.ndarray + Second-order section filters :func:`scipy.signal.sosfilt`. + phaseshift : (N,) numpy.ndarray + Phase shift in radians. + + Examples + -------- + .. plot:: + :context: close-figs + + delay, weight, sos, phaseshift, selection, secondary_source = \ + sfs.time.drivingfunction.nfchoa_25d_plane(array.x, R, npw, fs) + d = sfs.time.drivingfunction.nfchoa_25d_driving_signals( + delay, weight, sos, phaseshift, signal) + plot(d, selection, secondary_source) + + """ + if max_order is None: + max_order = util.max_order_circular_harmonics(len(x0)) + if c is None: + c = default.c + + x0 = util.asarray_of_rows(x0) + npw = util.asarray_1d(npw) + phi0, _, _ = util.cart2sph(*x0.T) + phipw, _, _ = util.cart2sph(*npw) + phaseshift = phi0 - phipw + np.pi + + delay = -r0 / c + weight = 2 + sos = [] + for m in range(max_order + 1): + _, p, _ = besselap(m, norm='delay') + s_zeros = np.zeros(m) + s_poles = c / r0 * p + s_gain = 1 + z_zeros, z_poles, z_gain = s2z(s_zeros, s_poles, s_gain, fs) + sos.append(zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest')) + selection = util.source_selection_all(len(x0)) + return delay, weight, sos, phaseshift, selection, secondary_source_point(c) + + +def nfchoa_25d_point(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): + r"""Virtual Point source by 2.5-dimensional NFC-HOA. + + .. math:: + + D(\phi_0, s) = + \frac{1}{2\pi r_\text{s}} + \e{\frac{s}{c}(r_0-r_\text{s})} + \sum_{m=-M}^{M} + \Big(\frac{s-\frac{c}{r_\text{s}}\sigma_0}{s-\frac{c}{r_0}\sigma_0}\Big)^\mu + \prod_{l=1}^{\nu} + \frac{(s-\frac{c}{r_\text{s}}\sigma_l)^2-(\frac{c}{r_\text{s}}\omega_l)^2} + {(s-\frac{c}{r_0}\sigma_l)^2+(\frac{c}{r_0}\omega_l)^2} + \e{\i m(\phi_0 - \phi_\text{s})} + + The driving function is represented in the Laplace domain, + from which the recursive filters are designed. + :math:`\sigma_l + \i\omega_l` denotes the complex roots of + the reverse Bessel polynomial. + The number of second-order sections is + :math:`\nu = \big\lfloor\tfrac{|m|}{2}\big\rfloor`, + whereas the number of first-order section :math:`\mu` is either 0 or 1 + for even and odd :math:`|m|`, respectively. + + Parameters + ---------- + x0 : (N, 3) array_like + Sequence of secondary source positions. + r0 : float + Radius of the circular secondary source distribution. + xs : (3,) array_like + Virtual source position. + fs : int + Sampling frequency in Hertz. + max_order : int, optional + Ambisonics order. + c : float, optional + Speed of sound in m/s. + s2z : callable, optional + Function transforming s-domain poles and zeros into z-domain, + e.g. :func:`matchedz_zpk`, :func:`scipy.signal.bilinear_zpk`. + + Returns + ------- + delay : float + Overall delay in seconds. + weight : float + Overall weight. + sos : list of numpy.ndarray + Second-order section filters :func:`scipy.signal.sosfilt`. + phaseshift : (N,) numpy.ndarray + Phase shift in radians. + + Examples + -------- + .. plot:: + :context: close-figs + + delay, weight, sos, phaseshift, selection, secondary_source = \ + sfs.time.drivingfunction.nfchoa_25d_point(array.x, R, xs, fs) + d = sfs.time.drivingfunction.nfchoa_25d_driving_signals( + delay, weight, sos, phaseshift, signal) + plot(d, selection, secondary_source, t=ts) + + """ + if max_order is None: + max_order = util.max_order_circular_harmonics(len(x0)) + if c is None: + c = default.c + + x0 = util.asarray_of_rows(x0) + xs = util.asarray_1d(xs) + phi0, _, _ = util.cart2sph(*x0.T) + phis, _, rs = util.cart2sph(*xs) + phaseshift = phi0 - phis + + delay = (rs - r0) / c + weight = 1 / 2 / np.pi / rs + sos = [] + for m in range(max_order + 1): + _, p, _ = besselap(m, norm='delay') + s_zeros = c / rs * p + s_poles = c / r0 * p + s_gain = 1 + z_zeros, z_poles, z_gain = s2z(s_zeros, s_poles, s_gain, fs) + sos.append(zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest')) + selection = util.source_selection_all(len(x0)) + return delay, weight, sos, phaseshift, selection, secondary_source_point(c) + + +def nfchoa_3d_plane(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk): + r"""Virtual plane wave by 3-dimensional NFC-HOA. + + .. math:: + + D(\phi_0, s) = + \frac{\e{\frac{s}{c}r_0}}{r_0} + \sum_{n=0}^{N} + (-1)^n (2n+1) P_{n}(\cos\Theta) + \Big(\frac{s}{s-\frac{c}{r_0}\sigma_0}\Big)^\mu + \prod_{l=1}^{\nu} + \frac{s^2}{(s-\frac{c}{r_0}\sigma_l)^2+(\frac{c}{r_0}\omega_l)^2} + + The driving function is represented in the Laplace domain, + from which the recursive filters are designed. + :math:`\sigma_l + \i\omega_l` denotes the complex roots of + the reverse Bessel polynomial. + The number of second-order sections is + :math:`\nu = \big\lfloor\tfrac{|m|}{2}\big\rfloor`, + whereas the number of first-order section :math:`\mu` is either 0 or 1 + for even and odd :math:`|m|`, respectively. + :math:`P_{n}(\cdot)` denotes the Legendre polynomial of degree :math:`n`, + and :math:`\Theta` the angle between :math:`(\theta, \phi)` + and :math:`(\theta_\text{pw}, \phi_\text{pw})`. + + Parameters + ---------- + x0 : (N, 3) array_like + Sequence of secondary source positions. + r0 : float + Radius of the spherical secondary source distribution. + npw : (3,) array_like + Unit vector (propagation direction) of plane wave. + fs : int + Sampling frequency in Hertz. + max_order : int, optional + Ambisonics order. + c : float, optional + Speed of sound in m/s. + s2z : callable, optional + Function transforming s-domain poles and zeros into z-domain, + e.g. :func:`matchedz_zpk`, :func:`scipy.signal.bilinear_zpk`. + + Returns + ------- + delay : float + Overall delay in seconds. + weight : float + Overall weight. + sos : list of numpy.ndarray + Second-order section filters :func:`scipy.signal.sosfilt`. + phaseshift : (N,) numpy.ndarray + Phase shift in radians. + + """ + if max_order is None: + max_order = util.max_order_spherical_harmonics(len(x0)) + if c is None: + c = default.c + + x0 = util.asarray_of_rows(x0) + npw = util.asarray_1d(npw) + phi0, theta0, _ = util.cart2sph(*x0.T) + phipw, thetapw, _ = util.cart2sph(*npw) + phaseshift = np.arccos(np.dot(x0 / r0, -npw)) + + delay = -r0 / c + weight = 4 * np.pi / r0 + sos = [] + for m in range(max_order + 1): + _, p, _ = besselap(m, norm='delay') + s_zeros = np.zeros(m) + s_poles = c / r0 * p + s_gain = 1 + z_zeros, z_poles, z_gain = s2z(s_zeros, s_poles, s_gain, fs) + sos.append(zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest')) + selection = util.source_selection_all(len(x0)) + return delay, weight, sos, phaseshift, selection, secondary_source_point(c) + + +def nfchoa_3d_point(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): + r"""Virtual point source by 3-dimensional NFC-HOA. + + .. math:: + + D(\phi_0, s) = + \frac{\e{\frac{s}{c}(r_0-r_\text{s})}}{4 \pi r_0 r_\text{s}} + \sum_{n=0}^{N} + (2n+1) P_{n}(\cos\Theta) + \Big(\frac{s-\frac{c}{r_\text{s}}\sigma_0}{s-\frac{c}{r_0}\sigma_0}\Big)^\mu + \prod_{l=1}^{\nu} + \frac{(s-\frac{c}{r_\text{s}}\sigma_l)^2-(\frac{c}{r_\text{s}}\omega_l)^2} + {(s-\frac{c}{r_0}\sigma_l)^2+(\frac{c}{r_0}\omega_l)^2} + + The driving function is represented in the Laplace domain, + from which the recursive filters are designed. + :math:`\sigma_l + \i\omega_l` denotes the complex roots of + the reverse Bessel polynomial. + The number of second-order sections is + :math:`\nu = \big\lfloor\tfrac{|m|}{2}\big\rfloor`, + whereas the number of first-order section :math:`\mu` is either 0 or 1 + for even and odd :math:`|m|`, respectively. + :math:`P_{n}(\cdot)` denotes the Legendre polynomial of degree :math:`n`, + and :math:`\Theta` the angle between :math:`(\theta, \phi)` + and :math:`(\theta_\text{s}, \phi_\text{s})`. + + Parameters + ---------- + x0 : (N, 3) array_like + Sequence of secondary source positions. + r0 : float + Radius of the spherial secondary source distribution. + xs : (3,) array_like + Virtual source position. + fs : int + Sampling frequency in Hertz. + max_order : int, optional + Ambisonics order. + c : float, optional + Speed of sound in m/s. + s2z : callable, optional + Function transforming s-domain poles and zeros into z-domain, + e.g. :func:`matchedz_zpk`, :func:`scipy.signal.bilinear_zpk`. + + Returns + ------- + delay : float + Overall delay in seconds. + weight : float + Overall weight. + sos : list of numpy.ndarray + Second-order section filters :func:`scipy.signal.sosfilt`. + phaseshift : (N,) numpy.ndarray + Phase shift in radians. + + """ + if max_order is None: + max_order = util.max_order_spherical_harmonics(len(x0)) + if c is None: + c = default.c + + x0 = util.asarray_of_rows(x0) + xs = util.asarray_1d(xs) + phi0, theta0, _ = util.cart2sph(*x0.T) + phis, thetas, rs = util.cart2sph(*xs) + phaseshift = np.arccos(np.dot(x0 / r0, xs / rs)) + + delay = (rs - r0) / c + weight = 1 / r0 / rs + sos = [] + for m in range(max_order + 1): + _, p, _ = besselap(m, norm='delay') + s_zeros = c / rs * p + s_poles = c / r0 * p + s_gain = 1 + z_zeros, z_poles, z_gain = s2z(s_zeros, s_poles, s_gain, fs) + sos.append(zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest')) + selection = util.source_selection_all(len(x0)) + return delay, weight, sos, phaseshift, selection, secondary_source_point(c) + + +def nfchoa_25d_driving_signals(delay, weight, sos, phaseshift, signal): + """Get 2.5-dimensional NFC-HOA driving signals. + + Parameters + ---------- + delay : float + Overall delay in seconds. + weight : float + Overall weight. + sos : list of array_like + Second-order section filters :func:`scipy.signal.sosfilt`. + phaseshift : (N,) array_like + Phase shift in radians. + signal : (L,) array_like + float + Excitation signal consisting of (mono) audio data and a sampling + rate (in Hertz). A `DelayedSignal` object can also be used. + + Returns + ------- + `DelayedSignal` + A tuple containing the delayed signals (in a `numpy.ndarray` + with shape ``(L, N)``), followed by the sampling rate (in Hertz) + and a (possibly negative) time offset (in seconds). + + """ + data, fs, t_offset = util.as_delayed_signal(signal) + N = len(phaseshift) + out = np.tile(np.expand_dims(sosfilt(sos[0], data), 1), (1, N)) + for m in range(1, len(sos)): + modal_response = sosfilt(sos[m], data)[:, np.newaxis] + out += modal_response * np.cos(m * phaseshift) + return util.DelayedSignal(2 * weight * out, fs, t_offset + delay) + + +def nfchoa_3d_driving_signals(delay, weight, sos, phaseshift, signal): + """Get 3-dimensional NFC-HOA driving signals. + + Parameters + ---------- + delay : float + Overall delay in seconds. + weight : float + Overall weight. + sos : list of array_like + Second-order section filters :func:`scipy.signal.sosfilt`. + phaseshift : (N,) array_like + Phase shift in radians. + signal : (L,) array_like + float + Excitation signal consisting of (mono) audio data and a sampling + rate (in Hertz). A `DelayedSignal` object can also be used. + + Returns + ------- + `DelayedSignal` + A tuple containing the delayed signals (in a `numpy.ndarray` + with shape ``(L, N)``), followed by the sampling rate (in Hertz) + and a (possibly negative) time offset (in seconds). + + """ + data, fs, t_offset = util.as_delayed_signal(signal) + N = len(phaseshift) + out = np.tile(np.expand_dims(sosfilt(sos[0], data), 1), (1, N)) + for m in range(1, len(sos)): + modal_response = sosfilt(sos[m], data)[:, np.newaxis] + out += (2 * m + 1) * modal_response * legendre(m, np.cos(phaseshift)) + return util.DelayedSignal(weight / 4 / np.pi * out, fs, t_offset + delay) diff --git a/sfs/time/source.py b/sfs/time/source.py index cfb6898..cc0154e 100644 --- a/sfs/time/source.py +++ b/sfs/time/source.py @@ -17,7 +17,8 @@ ts = rs / sfs.default.c # time-of-arrival at origin # Impulsive excitation - signal = unit_impulse(512), 44100 + fs = 44100 + signal = unit_impulse(512), fs grid = sfs.util.xyz_grid([-2, 3], [-1, 2], 0, spacing=0.02) diff --git a/sfs/util.py b/sfs/util.py index 8f02ef7..a97bfff 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -616,3 +616,45 @@ def source_selection_focused(ns, x0, xs): def source_selection_all(N): """Select all secondary sources.""" return np.ones(N, dtype=bool) + + +def max_order_circular_harmonics(N): + r"""Maximum order of 2D/2.5D HOA. + + It returns the maximum order for which no spatial aliasing appears. + It is given on page 132 of [Ahrens2012]_ as + + .. math:: + \text{max_order} = + \begin{cases} + N/2 - 1 & \text{even}\;N \\ + (N-1)/2 & \text{odd}\;N, + \end{cases} + + which is equivalent to + + .. math:: + \text{max_order} = \big\lfloor \frac{N - 1}{2} \big\rfloor. + + Parameters + ---------- + N : int + Number of secondary sources. + + """ + return (N - 1) // 2 + + +def max_order_spherical_harmonics(N): + r"""Maximum order of 3D HOA. + + .. math:: + \text{max_order} = \lfloor \sqrt{N} \rfloor - 1. + + Parameters + ---------- + N : int + Number of secondary sources. + + """ + return int(np.sqrt(N) - 1) From 8bb6387a7078c3ce654cd3790fe41b808230098f Mon Sep 17 00:00:00 2001 From: fs446 Date: Tue, 26 Feb 2019 16:05:35 +0100 Subject: [PATCH 042/101] examples in array docu, update Rostock WFS array data, PR 97 -Old "university_rostock.csv" as of 2015 is "wfs_university_rostock_2015.csv" now. New measurement data of december 2018 is stored in "wfs_university_rostock_2018" and now includes absolute height (z coordinate, KH120 logo) and weights of the 64 loudspeakers. --- data/arrays/example_array_4LS_2D.csv | 4 + data/arrays/example_array_6LS_3D.txt | 6 + ...ck.csv => wfs_university_rostock_2015.csv} | 0 data/arrays/wfs_university_rostock_2018.csv | 64 ++++++ doc/examples/sound_field_synthesis.py | 8 +- sfs/array.py | 186 +++++++++++++++--- 6 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 data/arrays/example_array_4LS_2D.csv create mode 100644 data/arrays/example_array_6LS_3D.txt rename data/arrays/{university_rostock.csv => wfs_university_rostock_2015.csv} (100%) create mode 100644 data/arrays/wfs_university_rostock_2018.csv 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)) From ce48b4229aab4ecc3682e103e94b28b07f3884fb Mon Sep 17 00:00:00 2001 From: Sascha Spors Date: Wed, 6 Mar 2019 17:39:31 +0100 Subject: [PATCH 043/101] Extended documentation of driving functions, added new examples --- sfs/mono/drivingfunction.py | 753 +++++++++++++++++++++++++++++++----- 1 file changed, 657 insertions(+), 96 deletions(-) diff --git a/sfs/mono/drivingfunction.py b/sfs/mono/drivingfunction.py index cd08f77..5dfb574 100644 --- a/sfs/mono/drivingfunction.py +++ b/sfs/mono/drivingfunction.py @@ -41,8 +41,34 @@ def plot(d, selection, secondary_source): def wfs_2d_line(omega, x0, n0, xs, c=None): - r"""Line source by 2-dimensional WFS. + r"""Driving function for 2-dimensional WFS for a virtual line source. + Parameters + ---------- + omega : float + Angular frequency of line source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs : (3,) array_like + Position of virtual line source. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- .. math:: D(\x_0,\w) = \frac{\i}{2} \wc @@ -71,8 +97,34 @@ def wfs_2d_line(omega, x0, n0, xs, c=None): def _wfs_point(omega, x0, n0, xs, c=None): - r"""Point source by two- or three-dimensional WFS. + r"""Driving function for 2/3-dimensional WFS for a virtual point source. + Parameters + ---------- + omega : float + Angular frequency of point source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs : (3,) array_like + Position of virtual point source. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- .. math:: D(\x_0, \w) = \i\wc \frac{\scalarprod{\x_0-\x_\text{s}}{\n_0}} @@ -104,8 +156,36 @@ def _wfs_point(omega, x0, n0, xs, c=None): def wfs_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): - r"""Point source by 2.5-dimensional WFS. + r"""Driving function for 2.5-dimensional WFS for a virtual point source. + Parameters + ---------- + omega : float + Angular frequency of point source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs : (3,) array_like + Position of virtual point source. + xref : (3,) array_like, optional + Reference point for synthesized sound field. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- .. math:: D(\x_0,\w) = \sqrt{\i\wc |\x_\text{ref}-\x_0|} @@ -142,8 +222,34 @@ def wfs_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): def _wfs_plane(omega, x0, n0, n=[0, 1, 0], c=None): - r"""Plane wave by two- or three-dimensional WFS. + r"""Driving function for 2/3-dimensional WFS for a virtual plane wave. + Parameters + ---------- + omega : float + Angular frequency of plane wave. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + n : (3,) array_like, optional + Normal vector (traveling direction) of plane wave. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- Eq.(17) from :cite:`Spors2008`: .. math:: @@ -175,8 +281,38 @@ def _wfs_plane(omega, x0, n0, n=[0, 1, 0], c=None): def wfs_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, omalias=None): - r"""Plane wave by 2.5-dimensional WFS. + r"""Driving function for 2.5-dimensional WFS for a virtual plane wave. + Parameters + ---------- + omega : float + Angular frequency of plane wave. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + n : (3,) array_like, optional + Normal vector (traveling direction) of plane wave. + xref : (3,) array_like, optional + Reference point for synthesized sound field. + c : float, optional + Speed of sound. + omalias: float, optional + Angular frequency where spatial aliasing becomes prominent. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- .. math:: D_\text{2.5D}(\x_0,\w) = \sqrt{\i\wc |\x_\text{ref}-\x_0|} @@ -210,8 +346,36 @@ def wfs_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, def _wfs_focused(omega, x0, n0, xs, ns, c=None): - r"""Focused source by two- or three-dimensional WFS. + r"""Driving function for 2/3-dimensional WFS for a focused source. + + Parameters + ---------- + omega : float + Angular frequency of focused source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs : (3,) array_like + Position of focused source. + ns : (3,) array_like + Direction of focused source. + c : float, optional + Speed of sound. + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- .. math:: D(\x_0,\w) = \i\wc \frac{\scalarprod{\x_0-\x_\text{s}}{\n_0}} @@ -244,8 +408,40 @@ def _wfs_focused(omega, x0, n0, xs, ns, c=None): def wfs_25d_focused(omega, x0, n0, xs, ns, xref=[0, 0, 0], c=None, omalias=None): - r"""Focused source by 2.5-dimensional WFS. + r"""Driving function for 2.5-dimensional WFS for a focused source. + Parameters + ---------- + omega : float + Angular frequency of focused source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs : (3,) array_like + Position of focused source. + ns : (3,) array_like + Direction of focused source. + xref : (3,) array_like, optional + Reference point for synthesized sound field. + c : float, optional + Speed of sound. + omalias: float, optional + Angular frequency where spatial aliasing becomes prominent. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- .. math:: D(\x_0,\w) = \sqrt{\i\wc |\x_\text{ref}-\x_0|} @@ -282,7 +478,32 @@ def wfs_25d_focused(omega, x0, n0, xs, ns, xref=[0, 0, 0], c=None, def wfs_25d_preeq(omega, omalias, c): - """Preqeualization for 2.5D WFS.""" + r"""Pre-equalization filter for 2.5-dimensional WFS. + + Parameters + ---------- + omega : float + Angular frequency. + omalias: float + Angular frequency where spatial aliasing becomes prominent. + c : float + Speed of sound. + + Returns + ------- + float + Complex weight for given angular frequency. + + Notes + ----- + .. math:: + + H(\w) = \begin{cases} + \sqrt{\i \wc} & \text{for } \w \leq \w_\text{alias} \\ + \sqrt{\i \frac{\w_\text{alias}}{c}} & \text{for } \w > \w_\text{alias} + \end{cases} + + """ if omalias is None: return np.sqrt(1j * util.wavenumber(omega, c)) else: @@ -293,7 +514,48 @@ def wfs_25d_preeq(omega, omalias, c): def delay_3d_plane(omega, x0, n0, n=[0, 1, 0], c=None): - """Plane wave by simple delay of secondary sources.""" + r"""Delay-only driving function for a virtual plane wave. + + Parameters + ---------- + omega : float + Angular frequency of plane wave. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + n : (3,) array_like, optional + Normal vector (traveling direction) of plane wave. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + .. math:: + + D(\x_0,\w) = \e{-\i\wc\scalarprod{\n}{\x_0}} + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.drivingfunction.delay_3d_plane( + omega, array.x, array.n, npw) + plot(d, selection, secondary_source) + + """ x0 = util.asarray_of_rows(x0) n = util.normalize_vector(n) k = util.wavenumber(omega, c) @@ -303,8 +565,36 @@ def delay_3d_plane(omega, x0, n0, n=[0, 1, 0], c=None): def nfchoa_2d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): - r"""Plane wave by two-dimensional NFC-HOA. + r"""Driving function for 2-dimensional NFC-HOA for a virtual plane wave. + Parameters + ---------- + omega : float + Angular frequency of plane wave. + x0 : (N, 3) array_like + Sequence of secondary source positions. + r0 : float + Radius of circular secondary source distribution. + n : (3,) array_like, optional + Normal vector (traveling direction) of plane wave. + max_order : float, optional + Maximum order of circular harmonics used for the calculation. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing only ``True`` indicating that + all secondary source are "active" for NFC-HOA. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- .. math:: D(\phi_0, \omega) = @@ -341,8 +631,36 @@ def nfchoa_2d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): def nfchoa_25d_point(omega, x0, r0, xs, max_order=None, c=None): - r"""Point source by 2.5-dimensional NFC-HOA. + r"""Driving function for 2.5-dimensional NFC-HOA for a virtual point source. + Parameters + ---------- + omega : float + Angular frequency of point source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + r0 : float + Radius of circular secondary source distribution. + xs : (3,) array_like + Position of point source. + max_order : float, optional + Maximum order of circular harmonics used for the calculation. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing only ``True`` indicating that + all secondary source are "active" for NFC-HOA. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- .. math:: D(\phi_0, \omega) = @@ -381,8 +699,36 @@ def nfchoa_25d_point(omega, x0, r0, xs, max_order=None, c=None): def nfchoa_25d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): - r"""Plane wave by 2.5-dimensional NFC-HOA. + r"""Driving function for 2.5-dimensional NFC-HOA for a virtual plane wave. + Parameters + ---------- + omega : float + Angular frequency of point source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + r0 : float + Radius of circular secondary source distribution. + n : (3,) array_like, optional + Normal vector (traveling direction) of plane wave. + max_order : float, optional + Maximum order of circular harmonics used for the calculation. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing only ``True`` indicating that + all secondary source are "active" for NFC-HOA. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- .. math:: D(\phi_0, \omega) = @@ -420,11 +766,47 @@ def nfchoa_25d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): def sdm_2d_line(omega, x0, n0, xs, c=None): - """Line source by two-dimensional SDM. + r"""Driving function for 2-dimensional SDM for a virtual line source. + + Parameters + ---------- + omega : float + Angular frequency of line source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs : (3,) array_like + Position of line source. + c : float, optional + Speed of sound. + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- The secondary sources have to be located on the x-axis (y0=0). Derived from :cite:`Spors2009`, Eq.(9), Eq.(4). + Examples + -------- + .. plot:: + :context: close-figs + + array = sfs.array.linear(32, 0.2, orientation=[0, -1, 0]) + d, selection, secondary_source = sfs.mono.drivingfunction.sdm_2d_line( + omega, array.x, array.n, xs) + plot(d, selection, secondary_source) + """ x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) @@ -432,12 +814,40 @@ def sdm_2d_line(omega, x0, n0, xs, c=None): k = util.wavenumber(omega, c) ds = x0 - xs r = np.linalg.norm(ds, axis=1) - return - 1j/2 * k * xs[1] / r * hankel2(1, k * r) + d = - 1j/2 * k * xs[1] / r * hankel2(1, k * r) + selection = util.source_selection_all(len(x0)) + return d, selection, secondary_source_line(omega, c) def sdm_2d_plane(omega, x0, n0, n=[0, 1, 0], c=None): - r"""Plane wave by two-dimensional SDM. + r"""Driving function for 2-dimensional SDM for a virtual plane wave. + Parameters + ---------- + omega : float + Angular frequency of plane wave. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + n: (3,) array_like, optional + Normal vector (traveling direction) of plane wave. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- The secondary sources have to be located on the x-axis (y0=0). Derived from :cite:`Ahrens2012`, Eq.(3.73), Eq.(C.5), Eq.(C.11): @@ -445,35 +855,127 @@ def sdm_2d_plane(omega, x0, n0, n=[0, 1, 0], c=None): D(\x_0,k) = k_\text{pw,y} \e{-\i k_\text{pw,x} x} + Examples + -------- + .. plot:: + :context: close-figs + + array = sfs.array.linear(32, 0.2, orientation=[0, -1, 0]) + d, selection, secondary_source = sfs.mono.drivingfunction.sdm_2d_plane( + omega, array.x, array.n, npw) + plot(d, selection, secondary_source) + """ x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) n = util.normalize_vector(n) k = util.wavenumber(omega, c) - return k * n[1] * np.exp(-1j * k * n[0] * x0[:, 0]) + d = k * n[1] * np.exp(-1j * k * n[0] * x0[:, 0]) + selection = util.source_selection_all(len(x0)) + return d, selection, secondary_source_line(omega, c) def sdm_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): - """Plane wave by 2.5-dimensional SDM. + r"""Driving function for 2.5-dimensional SDM for a virtual plane wave. + Parameters + ---------- + omega : float + Angular frequency of plane wave. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + n: (3,) array_like, optional + Normal vector (traveling direction) of plane wave. + xref : (3,) array_like, optional + Reference point for synthesized sound field. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- The secondary sources have to be located on the x-axis (y0=0). Eq.(3.79) from :cite:`Ahrens2012`. + Examples + -------- + .. plot:: + :context: close-figs + + array = sfs.array.linear(32, 0.2, orientation=[0, -1, 0]) + d, selection, secondary_source = ( + sfs.mono.drivingfunction.sdm_25d_plane( + omega, array.x, array.n, npw, [0, -1, 0])) + plot(d, selection, secondary_source) + """ x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) n = util.normalize_vector(n) xref = util.asarray_1d(xref) k = util.wavenumber(omega, c) - return 4j * np.exp(-1j*k*n[1]*xref[1]) / hankel2(0, k*n[1]*xref[1]) * \ + d = 4j * np.exp(-1j*k*n[1]*xref[1]) / hankel2(0, k*n[1]*xref[1]) * \ np.exp(-1j*k*n[0]*x0[:, 0]) + selection = util.source_selection_all(len(x0)) + return d, selection, secondary_source_point(omega, c) def sdm_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None): - """Point source by 2.5-dimensional SDM. + r"""Driving function for 2.5-dimensional SDM for a virtual point source. + Parameters + ---------- + omega : float + Angular frequency of point source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs: (3,) array_like + Position of virtual point source. + xref : (3,) array_like, optional + Reference point for synthesized sound field. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- The secondary sources have to be located on the x-axis (y0=0). - Driving funcnction from :cite:`Spors2010`, Eq.(24). + Driving function from :cite:`Spors2010`, Eq.(24). + + Examples + -------- + .. plot:: + :context: close-figs + + array = sfs.array.linear(32, 0.2, orientation=[0, -1, 0]) + d, selection, secondary_source = ( + sfs.mono.drivingfunction.sdm_25d_point( + omega, array.x, array.n, xs, [0, -1, 0])) + plot(d, selection, secondary_source) """ x0 = util.asarray_of_rows(x0) @@ -483,19 +985,19 @@ def sdm_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None): k = util.wavenumber(omega, c) ds = x0 - xs r = np.linalg.norm(ds, axis=1) - return 1/2 * 1j * k * np.sqrt(xref[1] / (xref[1] - xs[1])) * \ + d = 1/2 * 1j * k * np.sqrt(xref[1] / (xref[1] - xs[1])) * \ xs[1] / r * hankel2(1, k * r) + selection = util.source_selection_all(len(x0)) + return d, selection, secondary_source_point(omega, c) def esa_edge_2d_plane(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, c=None): - """Plane wave by two-dimensional ESA for an edge-shaped secondary source - distribution consisting of monopole line sources. - - One leg of the secondary sources has to be located on the x-axis (y0=0), - the edge at the origin. + r"""Driving function for 2-dimensional plane wave with edge ESA. - Derived from :cite:`Spors2016` + Driving function for a virtual plane wave using the 2-dimensional ESA + for an edge-shaped secondary source distribution consisting of + monopole line sources. Parameters ---------- @@ -515,8 +1017,21 @@ def esa_edge_2d_plane(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, Returns ------- - (N,) numpy.ndarray + d : (N,) numpy.ndarray Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + One leg of the secondary sources has to be located on the x-axis (y0=0), + the edge at the origin. + + Derived from :cite:`Spors2016` """ x0 = np.asarray(x0) @@ -543,18 +1058,17 @@ def esa_edge_2d_plane(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, d[phi > 0] = -d[phi > 0] - return 4*np.pi/alpha * d + selection = util.source_selection_all(len(x0)) + return 4*np.pi/alpha * d, selection, secondary_source_line(omega, c) def esa_edge_dipole_2d_plane(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, c=None): - """Plane wave by two-dimensional ESA for an edge-shaped secondary source - distribution consisting of dipole line sources. - - One leg of the secondary sources has to be located on the x-axis (y0=0), - the edge at the origin. + r"""Driving function for 2-dimensional plane wave with edge dipole ESA. - Derived from :cite:`Spors2016` + Driving function for a virtual plane wave using the 2-dimensional ESA + for an edge-shaped secondary source distribution consisting of + dipole line sources. Parameters ---------- @@ -574,8 +1088,21 @@ def esa_edge_dipole_2d_plane(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, Returns ------- - (N,) numpy.ndarray + d : (N,) numpy.ndarray Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + One leg of the secondary sources has to be located on the x-axis (y0=0), + the edge at the origin. + + Derived from :cite:`Spors2016` """ x0 = np.asarray(x0) @@ -604,13 +1131,11 @@ def esa_edge_dipole_2d_plane(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, def esa_edge_2d_line(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): - """Line source by two-dimensional ESA for an edge-shaped secondary source - distribution constisting of monopole line sources. + r"""Driving function for 2-dimensional line source with edge ESA. - One leg of the secondary sources have to be located on the x-axis (y0=0), - the edge at the origin. - - Derived from :cite:`Spors2016` + Driving function for a virtual line source using the 2-dimensional ESA + for an edge-shaped secondary source distribution consisting of line + sources. Parameters ---------- @@ -630,8 +1155,21 @@ def esa_edge_2d_line(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): Returns ------- - (N,) numpy.ndarray + d : (N,) numpy.ndarray Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + One leg of the secondary sources has to be located on the x-axis (y0=0), + the edge at the origin. + + Derived from :cite:`Spors2016` """ x0 = np.asarray(x0) @@ -662,29 +1200,25 @@ def esa_edge_2d_line(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): d[phi > 0] = -d[phi > 0] - return -1j*np.pi/alpha * d + selection = util.source_selection_all(len(x0)) + return -1j*np.pi/alpha * d, selection, secondary_source_line(omega, c) -def esa_edge_25d_point(omega, x0, xs, xref=[2, -2, 0], alpha=3/2*np.pi, - Nc=None, c=None): - """Point source by 2.5-dimensional ESA for an edge-shaped secondary source - distribution constisting of monopole line sources. - - One leg of the secondary sources have to be located on the x-axis (y0=0), - the edge at the origin. +def esa_edge_dipole_2d_line(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): + r"""Driving function for 2-dimensional line source with edge dipole ESA. - Derived from :cite:`Spors2016` + Driving function for a virtual line source using the 2-dimensional ESA + for an edge-shaped secondary source distribution consisting of dipole line + sources. Parameters ---------- omega : float Angular frequency. - x0 : int(N, 3) array_like + x0 : (N, 3) array_like Sequence of secondary source positions. xs : (3,) array_like Position of synthesized line source. - xref: (3,) array_like or float - Reference position or reference distance alpha : float, optional Outer angle of edge. Nc : int, optional @@ -695,40 +1229,70 @@ def esa_edge_25d_point(omega, x0, xs, xref=[2, -2, 0], alpha=3/2*np.pi, Returns ------- - (N,) numpy.ndarray + d : (N,) numpy.ndarray Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + One leg of the secondary sources has to be located on the x-axis (y0=0), + the edge at the origin. + + Derived from :cite:`Spors2016` """ x0 = np.asarray(x0) - xs = np.asarray(xs) - xref = np.asarray(xref) + k = util.wavenumber(omega, c) + phi_s = np.arctan2(xs[1], xs[0]) + if phi_s < 0: + phi_s = phi_s + 2*np.pi + r_s = np.linalg.norm(xs) + L = x0.shape[0] - if np.isscalar(xref): - a = np.linalg.norm(xref)/np.linalg.norm(xref-xs) - else: - a = np.linalg.norm(xref-x0, axis=1)/np.linalg.norm(xref-xs) + r = np.linalg.norm(x0, axis=1) + phi = np.arctan2(x0[:, 1], x0[:, 0]) + phi = np.where(phi < 0, phi+2*np.pi, phi) - return 1j*np.sqrt(a) * esa_edge_2d_line(omega, x0, xs, alpha=alpha, Nc=Nc, - c=c) + if Nc is None: + Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) + epsilon = np.ones(Nc) # weights for series expansion + epsilon[0] = 2 -def esa_edge_dipole_2d_line(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): - """Line source by two-dimensional ESA for an edge-shaped secondary source - distribution constisting of dipole line sources. + d = np.zeros(L, dtype=complex) + idx = (r <= r_s) + for m in np.arange(Nc): + nu = m*np.pi/alpha + f = 1/epsilon[m] * np.cos(nu*phi_s) * np.cos(nu*phi) + d[idx] = d[idx] + f[idx] * jn(nu, k*r[idx]) * hankel2(nu, k*r_s) + d[~idx] = d[~idx] + f[~idx] * jn(nu, k*r_s) * hankel2(nu, k*r[~idx]) - One leg of the secondary sources have to be located on the x-axis (y0=0), - the edge at the origin. + return -1j*np.pi/alpha * d - Derived from :cite:`Spors2016` + +def esa_edge_25d_point(omega, x0, xs, xref=[2, -2, 0], alpha=3/2*np.pi, + Nc=None, c=None): + r"""Driving function for 2.5-dimensional point source with edge ESA. + + Driving function for a virtual point source using the 2.5-dimensional + ESA for an edge-shaped secondary source distribution consisting of point + sources. Parameters ---------- omega : float Angular frequency. - x0 : (N, 3) array_like + x0 : int(N, 3) array_like Sequence of secondary source positions. xs : (3,) array_like Position of synthesized line source. + xref: (3,) array_like or float + Reference position or reference distance alpha : float, optional Outer angle of edge. Nc : int, optional @@ -739,37 +1303,34 @@ def esa_edge_dipole_2d_line(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): Returns ------- - (N,) numpy.ndarray + d : (N,) numpy.ndarray Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + One leg of the secondary sources has to be located on the x-axis (y0=0), + the edge at the origin. + + Derived from :cite:`Spors2016` """ x0 = np.asarray(x0) - k = util.wavenumber(omega, c) - phi_s = np.arctan2(xs[1], xs[0]) - if phi_s < 0: - phi_s = phi_s + 2*np.pi - r_s = np.linalg.norm(xs) - L = x0.shape[0] - - r = np.linalg.norm(x0, axis=1) - phi = np.arctan2(x0[:, 1], x0[:, 0]) - phi = np.where(phi < 0, phi+2*np.pi, phi) - - if Nc is None: - Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) - - epsilon = np.ones(Nc) # weights for series expansion - epsilon[0] = 2 + xs = np.asarray(xs) + xref = np.asarray(xref) - d = np.zeros(L, dtype=complex) - idx = (r <= r_s) - for m in np.arange(Nc): - nu = m*np.pi/alpha - f = 1/epsilon[m] * np.cos(nu*phi_s) * np.cos(nu*phi) - d[idx] = d[idx] + f[idx] * jn(nu, k*r[idx]) * hankel2(nu, k*r_s) - d[~idx] = d[~idx] + f[~idx] * jn(nu, k*r_s) * hankel2(nu, k*r[~idx]) + if np.isscalar(xref): + a = np.linalg.norm(xref)/np.linalg.norm(xref-xs) + else: + a = np.linalg.norm(xref-x0, axis=1)/np.linalg.norm(xref-xs) - return -1j*np.pi/alpha * d + d, selection, _ = esa_edge_2d_line(omega, x0, xs, alpha=alpha, Nc=Nc, c=c) + return 1j*np.sqrt(a) * d, selection, secondary_source_point(omega, c) def secondary_source_point(omega, c): From 44a2c5e3154119bff5da97c2aeab28dcaa30fe6b Mon Sep 17 00:00:00 2001 From: Nara Hahn Date: Thu, 27 Dec 2018 00:19:32 +0100 Subject: [PATCH 044/101] Sound field of a pulsating sphere --- .travis.yml | 1 + doc/examples.rst | 1 + .../animations-pulsating-sphere.ipynb | 366 ++++++++++++++++++ doc/examples/animations_pulsating_sphere.py | 114 ++++++ sfs/mono/source.py | 109 ++++++ sfs/plot.py | 8 +- 6 files changed, 595 insertions(+), 4 deletions(-) create mode 100644 doc/examples/animations-pulsating-sphere.ipynb create mode 100644 doc/examples/animations_pulsating_sphere.py diff --git a/.travis.yml b/.travis.yml index e92c3ac..c60bab6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ addons: apt: packages: - pandoc + - ffmpeg install: - pip install . - pip install -r tests/requirements.txt diff --git a/doc/examples.rst b/doc/examples.rst index be11b0d..3a20de9 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -15,4 +15,5 @@ Examples examples/modal-room-acoustics examples/mirror-image-source-model + examples/animations-pulsating-sphere example-python-scripts diff --git a/doc/examples/animations-pulsating-sphere.ipynb b/doc/examples/animations-pulsating-sphere.ipynb new file mode 100644 index 0000000..a0d7da2 --- /dev/null +++ b/doc/examples/animations-pulsating-sphere.ipynb @@ -0,0 +1,366 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Animations of a Pulsating Sphere" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sfs\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from IPython.display import HTML" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this example, the sound field of a pulsating sphere is visualized.\n", + "Different acoustic variables, such as sound pressure,\n", + "particle velocity, and particle displacement, are simulated.\n", + "The first two quantities are computed with\n", + "\n", + "- [sfs.mono.source.pulsating_sphere()](../sfs.mono.source.rst#sfs.mono.source.pulsating_sphere) and \n", + "- [sfs.mono.source.pulsating_sphere_velocity()](../sfs.mono.source.rst#sfs.mono.source.pulsating_sphere_velocity)\n", + "\n", + "while the last one can be obtained by using\n", + "\n", + "- [sfs.util.displacement()](../sfs.util.rst#sfs.util.displacement)\n", + "\n", + "which converts the particle velocity into displacement.\n", + "\n", + "A couple of additional functions are implemented in\n", + "\n", + "- [animations_pulsating_sphere.py](animations_pulsating_sphere.py)\n", + "\n", + "in order to help creating animating pictures, which is fun!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import animations_pulsating_sphere as animation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Pulsating sphere\n", + "center = [0, 0, 0]\n", + "radius = 0.25\n", + "amplitude = 0.05\n", + "f = 1000 # frequency\n", + "omega = 2 * np.pi * f # angular frequency\n", + "\n", + "# Axis limits\n", + "figsize = (6, 6)\n", + "xmin, xmax = -1, 1\n", + "ymin, ymax = -1, 1\n", + "\n", + "# Animations\n", + "frames = 20 # frames per period" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Particle Displacement" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.025)\n", + "ani = animation.particle_displacement(\n", + " omega, center, radius, amplitude, grid, frames, figsize, c='Gray')\n", + "plt.close()\n", + "HTML(ani.to_html5_video())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can play with the animation more interactively by using `.to_jshtml`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "HTML(ani.to_jshtml())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Of course, different types of grid can be chosen.\n", + "Below is the particle animation using the same parameters\n", + "but with a [hexagonal grid](https://www.redblobgames.com/grids/hexagons/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def hex_grid(xlim, ylim, hex_edge, align='horizontal'):\n", + " if align is 'vertical':\n", + " umin, umax = ylim\n", + " vmin, vmax = xlim\n", + " else:\n", + " umin, umax = xlim\n", + " vmin, vmax = ylim\n", + " du = np.sqrt(3) * hex_edge\n", + " dv = 1.5 * hex_edge\n", + " num_u = int((umax - umin) / du)\n", + " num_v = int((vmax - vmin) / dv)\n", + " u, v = np.meshgrid(np.linspace(umin, umax, num_u),\n", + " np.linspace(vmin, vmax, num_v))\n", + " u[::2] += 0.5 * du\n", + "\n", + " if align is 'vertical':\n", + " grid = v, u, 0\n", + " elif align is 'horizontal':\n", + " grid = u, v, 0\n", + " return grid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "grid = hex_grid([xmin, xmax], [ymin, ymax], 0.0125, 'vertical')\n", + "ani = animation.particle_displacement(\n", + " omega, center, radius, amplitude, grid, frames, figsize, c='Gray')\n", + "plt.close()\n", + "HTML(ani.to_html5_video())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Another one using a random grid." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "grid = [np.random.uniform(xmin, xmax, 4000),\n", + " np.random.uniform(ymin, ymax, 4000), 0]\n", + "ani = animation.particle_displacement(\n", + " omega, center, radius, amplitude, grid, frames, figsize, c='Gray')\n", + "plt.close()\n", + "HTML(ani.to_html5_video())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Each grid has its strengths and weaknesses. Please refer to the\n", + "[on-line discussion](https://github.com/sfstoolbox/sfs-python/pull/69#issuecomment-468405536)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Particle Velocity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "amplitude = 1e-3\n", + "grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.04)\n", + "ani = animation.particle_velocity(\n", + " omega, center, radius, amplitude, grid, frames, figsize)\n", + "plt.close()\n", + "HTML(ani.to_html5_video())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Please notice that the amplitude of the pulsating motion is adjusted\n", + "so that the arrows are neither too short nor too long.\n", + "This kind of compromise is inevitable since\n", + "\n", + "$$\n", + "\\text{(particle velocity)} = \\text{i} \\omega \\times (\\text{amplitude}),\n", + "$$\n", + "\n", + "thus the absolute value of particle velocity is usually\n", + "much larger than that of amplitude.\n", + "It should be also kept in mind that the hole in the middle\n", + "does not visualizes the exact motion of the pulsating sphere.\n", + "According to the above equation, the actual amplitude should be\n", + "much smaller than the arrow lengths.\n", + "The changing rate of its size is also two times higher than the original frequency." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sound Pressure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "amplitude = 0.05\n", + "impedance_pw = sfs.default.rho0 * sfs.default.c\n", + "max_pressure = omega * impedance_pw * amplitude\n", + "\n", + "grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.005)\n", + "ani = animation.sound_pressure(\n", + " omega, center, radius, amplitude, grid, frames, pulsate=True,\n", + " figsize=figsize, vmin=-max_pressure, vmax=max_pressure)\n", + "plt.close()\n", + "HTML(ani.to_html5_video())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notice that the sound pressure exceeds\n", + "the atmospheric pressure ($\\approx 10^5$ Pa), which of course makes no sense.\n", + "This is due to the large amplitude (50 mm) of the pulsating motion.\n", + "It was chosen to better visualize the particle movements\n", + "in the earlier animations.\n", + "\n", + "For 1 kHz, the amplitude corresponding to a moderate sound pressure,\n", + "let say 1 Pa, is in the order of micrometer.\n", + "As it is very small compared to the corresponding wavelength (0.343 m),\n", + "the movement of the particles and the spatial structure of the sound field\n", + "cannot be observed simultaneously.\n", + "Furthermore, at high frequencies, the sound pressure\n", + "for a given particle displacement scales with the frequency.\n", + "The smaller wavelength (higher frequency) we choose,\n", + "it is more likely to end up with a prohibitively high sound pressure.\n", + "\n", + "In the following examples, the amplitude is set to a realistic value 1 $\\mu$m.\n", + "Notice that the pulsating motion of the sphere is no more visible." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "amplitude = 1e-6\n", + "impedance_pw = sfs.default.rho0 * sfs.default.c\n", + "max_pressure = omega * impedance_pw * amplitude\n", + "\n", + "grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.005)\n", + "ani = animation.sound_pressure(\n", + " omega, center, radius, amplitude, grid, frames, pulsate=True,\n", + " figsize=figsize, vmin=-max_pressure, vmax=max_pressure)\n", + "plt.close()\n", + "HTML(ani.to_html5_video())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's zoom in closer to the boundary of the sphere." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "L = 10 * amplitude\n", + "xmin_zoom, xmax_zoom = radius - L, radius + L\n", + "ymin_zoom, ymax_zoom = -L, L" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "grid = sfs.util.xyz_grid([xmin_zoom, xmax_zoom], [ymin_zoom, ymax_zoom], 0, spacing=L / 100)\n", + "ani = animation.sound_pressure(\n", + " omega, center, radius, amplitude, grid, frames, pulsate=True,\n", + " figsize=figsize, vmin=-max_pressure, vmax=max_pressure)\n", + "plt.close()\n", + "HTML(ani.to_html5_video())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This shows how the vibrating motion of the sphere (left half)\n", + "changes the sound pressure of the surrounding air (right half).\n", + "Notice that the sound pressure increases/decreases (more red/blue)\n", + "when the surface accelerates/decelerates." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [default]", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/doc/examples/animations_pulsating_sphere.py b/doc/examples/animations_pulsating_sphere.py new file mode 100644 index 0000000..712d4fa --- /dev/null +++ b/doc/examples/animations_pulsating_sphere.py @@ -0,0 +1,114 @@ +"""Animations of pulsating sphere.""" +import sfs +import numpy as np +from matplotlib import pyplot as plt +from matplotlib import animation + + +def particle_displacement(omega, center, radius, amplitude, grid, frames, + figsize=(8, 8), interval=80, blit=True, **kwargs): + """Generate sound particle animation.""" + velocity = sfs.mono.source.pulsating_sphere_velocity( + omega, center, radius, amplitude, grid) + displacement = sfs.util.displacement(velocity, omega) + phasor = np.exp(1j * 2 * np.pi / frames) + + fig, ax = plt.subplots(figsize=figsize) + ax.axis([grid[0].min(), grid[0].max(), grid[1].min(), grid[1].max()]) + scat = sfs.plot.particles(grid + displacement, **kwargs) + + def update_frame_displacement(i): + position = (grid + displacement * phasor**i).apply(np.real) + position = np.column_stack([position[0].flatten(), + position[1].flatten()]) + scat.set_offsets(position) + return [scat] + + return animation.FuncAnimation( + fig, update_frame_displacement, frames, + interval=interval, blit=blit) + + +def particle_velocity(omega, center, radius, amplitude, grid, frames, + figsize=(8, 8), interval=80, blit=True, **kwargs): + """Generate particle velocity animation.""" + velocity = sfs.mono.source.pulsating_sphere_velocity( + omega, center, radius, amplitude, grid) + phasor = np.exp(1j * 2 * np.pi / frames) + + fig, ax = plt.subplots(figsize=figsize) + ax.axis([grid[0].min(), grid[0].max(), grid[1].min(), grid[1].max()]) + quiv = sfs.plot.vectors( + velocity, grid, clim=[-omega * amplitude, omega * amplitude], + **kwargs) + + def update_frame_velocity(i): + quiv.set_UVC(*(velocity[:2] * phasor**i).apply(np.real)) + return [quiv] + + return animation.FuncAnimation( + fig, update_frame_velocity, frames, interval=interval, blit=True) + + +def sound_pressure(omega, center, radius, amplitude, grid, frames, + pulsate=False, figsize=(8, 8), interval=80, blit=True, + **kwargs): + """Generate sound pressure animation.""" + pressure = sfs.mono.source.pulsating_sphere( + omega, center, radius, amplitude, grid, inside=pulsate) + phasor = np.exp(1j * 2 * np.pi / frames) + + fig, ax = plt.subplots(figsize=figsize) + im = sfs.plot.soundfield(np.real(pressure), grid, **kwargs) + ax.axis([grid[0].min(), grid[0].max(), grid[1].min(), grid[1].max()]) + + def update_frame_pressure(i): + distance = np.linalg.norm(grid) + p = pressure * phasor**i + if pulsate: + p[distance <= radius + amplitude * np.real(phasor**i)] = np.nan + im.set_array(np.real(p)) + return [im] + + return animation.FuncAnimation( + fig, update_frame_pressure, frames, interval=interval, blit=True) + + +if __name__ == '__main__': + + # Pulsating sphere + center = [0, 0, 0] + radius = 0.25 + f = 750 # frequency + omega = 2 * np.pi * f # angular frequency + + # Axis limits + xmin, xmax = -1, 1 + ymin, ymax = -1, 1 + + # Animations + frames = 20 # frames per period + + # Particle displacement + amplitude = 5e-2 # amplitude of the surface displacement + grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.025) + ani = particle_displacement( + omega, center, radius, amplitude, grid, frames, c='Gray') + ani.save('pulsating_sphere_displacement.gif', dpi=80, writer='imagemagick') + + # Particle velocity + amplitude = 1e-3 # amplitude of the surface displacement + grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.04) + ani = particle_velocity( + omega, center, radius, amplitude, grid, frames) + ani.save('pulsating_sphere_velocity.gif', dpi=80, writer='imagemagick') + + # Sound pressure + amplitude = 1e-6 # amplitude of the surface displacement + impedance_pw = sfs.default.rho0 * sfs.default.c + max_pressure = omega * impedance_pw * amplitude + grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.005) + ani = sound_pressure( + omega, center, radius, amplitude, grid, frames, pulsate=True, + colorbar=True, vmin=-max_pressure, vmax=max_pressure) + ani.save('pulsating_sphere_pressure.gif', dpi=80, writer='imagemagick') diff --git a/sfs/mono/source.py b/sfs/mono/source.py index 05ad6cd..e3d808e 100644 --- a/sfs/mono/source.py +++ b/sfs/mono/source.py @@ -707,6 +707,115 @@ def plane_averaged_intensity(omega, x0, n0, grid, c=None, rho0=None): return util.XyzComponents([i * n for n in n0]) +def pulsating_sphere(omega, center, radius, amplitude, grid, inside=False, + c=None): + """Sound pressure of a pulsating sphere. + + Parameters + --------- + omega : float + Frequency of pulsating sphere + center : (3,) array_like + Center of sphere. + radius : float + Radius of sphere. + amplitude : float + Amplitude of displacement. + grid : triple of array_like + The grid that is used for the sound field calculations. + See `sfs.util.xyz_grid()`. + inside : bool, optional + As default, `numpy.nan` is returned for inside the sphere. + If ``inside=True``, the sound field inside the sphere is extrapolated. + c : float, optional + Speed of sound. + + Returns + ------- + numpy.ndarray + Sound pressure at positions given by *grid*. + If ``inside=False``, `numpy.nan` is returned for inside the sphere. + + Examples + -------- + + .. plot:: + :context: close-figs + + radius = 0.25 + amplitude = 1 / (radius * omega * sfs.default.rho0 * sfs.default.c) + p = sfs.mono.source.pulsating_sphere(omega, x0, radius, amplitude, grid) + sfs.plot.soundfield(p, grid) + plt.title("Sound Pressure of a Pulsating Sphere") + + """ + if c is None: + c = default.c + k = util.wavenumber(omega, c) + center = util.asarray_1d(center) + grid = util.as_xyz_components(grid) + + distance = np.linalg.norm(grid - center) + theta = np.arctan(1, k * distance) + impedance = default.rho0 * c * np.cos(theta) * np.exp(1j * theta) + radial_velocity = 1j * omega * amplitude * radius / distance \ + * np.exp(-1j * k * (distance - radius)) + if not inside: + radial_velocity[distance <= radius] = np.nan + return impedance * radial_velocity + + +def pulsating_sphere_velocity(omega, center, radius, amplitude, grid, c=None): + """Particle velocity of a pulsating sphere. + + Parameters + --------- + omega : float + Frequency of pulsating sphere + center : (3,) array_like + Center of sphere. + radius : float + Radius of sphere. + amplitude : float + Amplitude of displacement. + grid : triple of array_like + The grid that is used for the sound field calculations. + See `sfs.util.xyz_grid()`. + c : float, optional + Speed of sound. + + Returns + ------- + `XyzComponents` + Particle velocity at positions given by *grid*. + `numpy.nan` is returned for inside the sphere. + + Examples + -------- + + .. plot:: + :context: close-figs + + v = sfs.mono.source.pulsating_sphere_velocity(omega, x0, radius, amplitude, vgrid) + sfs.plot.soundfield(p, grid) + sfs.plot.vectors(v, vgrid) + plt.title("Sound Pressure and Particle Velocity of a Pulsating Sphere") + + """ + if c is None: + c = default.c + k = util.wavenumber(omega, c) + grid = util.as_xyz_components(grid) + + center = util.asarray_1d(center) + offset = grid - center + distance = np.linalg.norm(offset) + radial_velocity = 1j * omega * amplitude * radius / distance \ + * np.exp(-1j * k * (distance - radius)) + radial_velocity[distance <= radius] = np.nan + return util.XyzComponents([radial_velocity * o / distance for o in offset]) + + def _duplicate_zdirection(p, grid): """If necessary, duplicate field in z-direction.""" gridshape = np.broadcast(*grid).shape diff --git a/sfs/plot.py b/sfs/plot.py index 2e96d21..90d6685 100644 --- a/sfs/plot.py +++ b/sfs/plot.py @@ -328,8 +328,8 @@ def level(p, grid, xnorm=None, power=False, cmap=None, vmax=3, vmin=-50, def particles(x, trim=None, ax=None, xlabel='x (m)', ylabel='y (m)', - edgecolor='', **kwargs): - """Plot particle positions as scatter plot.""" + edgecolor='', marker='.', s=15, **kwargs): + """Plot particle positions as scatter plot""" XX, YY = [np.real(c) for c in x[:2]] if trim is not None: @@ -342,12 +342,12 @@ def particles(x, trim=None, ax=None, xlabel='x (m)', ylabel='y (m)', if ax is None: ax = plt.gca() - ax.scatter(XX, YY, edgecolor=edgecolor, **kwargs) - if xlabel: ax.set_xlabel(xlabel) if ylabel: ax.set_ylabel(ylabel) + return ax.scatter(XX, YY, edgecolor=edgecolor, marker=marker, s=s, + **kwargs) def vectors(v, grid, cmap='blacktransparent', headlength=3, headaxislength=2.5, From 41f520f58b021af2439d69a68eedeb9d8a4e9f63 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 11 Mar 2019 13:23:10 +0100 Subject: [PATCH 045/101] DOC: Escape underscore in \text{} Unescaped underscores seem to work with MathJax, but LaTeX itself does complain. --- sfs/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sfs/util.py b/sfs/util.py index a97bfff..3bb1eda 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -625,7 +625,7 @@ def max_order_circular_harmonics(N): It is given on page 132 of [Ahrens2012]_ as .. math:: - \text{max_order} = + \text{max\_order} = \begin{cases} N/2 - 1 & \text{even}\;N \\ (N-1)/2 & \text{odd}\;N, @@ -634,7 +634,7 @@ def max_order_circular_harmonics(N): which is equivalent to .. math:: - \text{max_order} = \big\lfloor \frac{N - 1}{2} \big\rfloor. + \text{max\_order} = \big\lfloor \frac{N - 1}{2} \big\rfloor. Parameters ---------- @@ -649,7 +649,7 @@ def max_order_spherical_harmonics(N): r"""Maximum order of 3D HOA. .. math:: - \text{max_order} = \lfloor \sqrt{N} \rfloor - 1. + \text{max\_order} = \lfloor \sqrt{N} \rfloor - 1. Parameters ---------- From 9012b205570143204ed444c804d7065b69ab81ef Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 11 Mar 2019 13:38:12 +0100 Subject: [PATCH 046/101] Update setup.cfg --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 3c6e79c..0c9e0fc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ -[bdist_wheel] -universal=1 +[metadata] +license_file = LICENSE From 1af1cbdf7987bd3c1f1d93756faf83283dc205b9 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 11 Mar 2019 18:23:22 +0100 Subject: [PATCH 047/101] TST: Use Ubuntu Xenial for all Python version on Travis-CI --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c60bab6..113bf3b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: python +dist: xenial matrix: include: - python: "3.5" - python: "3.6" - python: "3.7" - dist: xenial # 3.7 requires Xenial on Travis addons: apt: packages: From 33562d37dbfc78cdc300789d5fa61616e476e031 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 11 Mar 2019 13:43:01 +0100 Subject: [PATCH 048/101] Copyright year update --- LICENSE | 2 +- doc/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 385a6a6..800c691 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2016 SFS Toolbox Developers +Copyright (c) 2014-2019 SFS Toolbox Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/doc/conf.py b/doc/conf.py index 8934794..cf73857 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -107,7 +107,7 @@ # General information about the project. authors = 'SFS Toolbox Developers' project = 'SFS Toolbox' -copyright = '2017, ' + authors +copyright = '2019, ' + authors # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From eec07edfd275f88c2a2166f8263ec7c905a972cf Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 11 Mar 2019 21:37:07 +0100 Subject: [PATCH 049/101] Clip loudspeaker numbers --- sfs/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sfs/plot.py b/sfs/plot.py index 90d6685..a8b4d59 100644 --- a/sfs/plot.py +++ b/sfs/plot.py @@ -152,7 +152,7 @@ def loudspeaker_2d(x0, n0, a0=0.5, size=0.08, show_numbers=False, grid=None, for idx, (x00, n00) in enumerate(util.broadcast_zip(x0, n0)): x, y = x00[:2] - 1.2 * size * n00[:2] ax.text(x, y, idx + 1, horizontalalignment='center', - verticalalignment='center') + verticalalignment='center', clip_on=True) def _visible_secondarysources_2d(x0, n0, grid): From 552c3065bb6bff8fa05df565a2e68f3cfdf2dab4 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 11 Mar 2019 13:32:29 +0100 Subject: [PATCH 050/101] DOC: Update a few URIs --- CONTRIBUTING.rst | 2 +- doc/conf.py | 2 +- sfs/util.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5f2b388..19af082 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -81,6 +81,6 @@ New releases are made using the following steps: #. Check that the new release was built correctly on RTD_, delete the "stable" version and select the new release as default version -.. _twine: https://pypi.python.org/pypi/twine +.. _twine: https://pypi.org/project/twine/ .. _add release notes: https://github.com/sfstoolbox/sfs-python/tags .. _RTD: http://readthedocs.org/projects/sfs-python/builds/ diff --git a/doc/conf.py b/doc/conf.py index cf73857..f4e4735 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -74,7 +74,7 @@ 'python': ('https://docs.python.org/3/', None), 'numpy': ('https://docs.scipy.org/doc/numpy/', None), 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), - 'matplotlib': ('http://matplotlib.org/', None), + 'matplotlib': ('https://matplotlib.org/', None), } plot_include_source = True diff --git a/sfs/util.py b/sfs/util.py index 3bb1eda..dbfc307 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -546,7 +546,7 @@ def _count_walls_1d(a): def spherical_hn2(n, z): r"""Spherical Hankel function of 2nd kind. - Defined as http://dlmf.nist.gov/10.47.E6, + Defined as https://dlmf.nist.gov/10.47.E6, .. math:: From 66dc41cf6ebe42f59687fb08d2fca5961009dfff Mon Sep 17 00:00:00 2001 From: Frank Schultz Date: Wed, 13 Mar 2019 14:24:28 +0100 Subject: [PATCH 051/101] New mono drivingfunction handling (#125) --- doc/examples/horizontal_plane_arrays.py | 36 +- doc/examples/sound_field_synthesis.py | 22 +- doc/examples/soundfigures.py | 2 +- sfs/mono/__init__.py | 37 +- sfs/mono/drivingfunction.py | 1351 ----------------------- sfs/mono/esa.py | 358 ++++++ sfs/mono/nfchoa.py | 236 ++++ sfs/mono/sdm.py | 254 +++++ sfs/mono/soundfigure.py | 46 - sfs/mono/wfs.py | 604 ++++++++++ 10 files changed, 1512 insertions(+), 1434 deletions(-) delete mode 100644 sfs/mono/drivingfunction.py create mode 100644 sfs/mono/esa.py create mode 100644 sfs/mono/nfchoa.py create mode 100644 sfs/mono/sdm.py delete mode 100644 sfs/mono/soundfigure.py create mode 100644 sfs/mono/wfs.py diff --git a/doc/examples/horizontal_plane_arrays.py b/doc/examples/horizontal_plane_arrays.py index 75b07f9..c2abeab 100644 --- a/doc/examples/horizontal_plane_arrays.py +++ b/doc/examples/horizontal_plane_arrays.py @@ -46,34 +46,34 @@ def compute_and_plot_soundfield(title): # linear array, secondary point sources, virtual monopole array = sfs.array.linear(N, dx, center=acenter, orientation=anormal) -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_3d_point( +d, selection, secondary_source = sfs.mono.wfs.point_3d( omega, array.x, array.n, xs) compute_and_plot_soundfield('linear_ps_wfs_3d_point') -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_point( +d, selection, secondary_source = sfs.mono.wfs.point_25d( omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('linear_ps_wfs_25d_point') -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_2d_point( +d, selection, secondary_source = sfs.mono.wfs.point_2d( omega, array.x, array.n, xs) compute_and_plot_soundfield('linear_ps_wfs_2d_point') # linear array, secondary line sources, virtual line source -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_2d_line( +d, selection, secondary_source = sfs.mono.wfs.line_2d( omega, array.x, array.n, xs) compute_and_plot_soundfield('linear_ls_wfs_2d_line') # linear array, secondary point sources, virtual plane wave -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_3d_plane( +d, selection, secondary_source = sfs.mono.wfs.plane_3d( omega, array.x, array.n, npw) compute_and_plot_soundfield('linear_ps_wfs_3d_plane') -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_plane( +d, selection, secondary_source = sfs.mono.wfs.plane_25d( omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('linear_ps_wfs_25d_plane') -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_2d_plane( +d, selection, secondary_source = sfs.mono.wfs.plane_2d( omega, array.x, array.n, npw) compute_and_plot_soundfield('linear_ps_wfs_2d_plane') @@ -82,11 +82,11 @@ def compute_and_plot_soundfield(title): array = sfs.array.linear_diff(N//3 * [dx] + N//3 * [dx/2] + N//3 * [dx], center=acenter, orientation=anormal) -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_point( +d, selection, secondary_source = sfs.mono.wfs.point_25d( omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('linear_nested_ps_wfs_25d_point') -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_plane( +d, selection, secondary_source = sfs.mono.wfs.plane_25d( omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('linear_nested_ps_wfs_25d_plane') @@ -95,22 +95,22 @@ def compute_and_plot_soundfield(title): array = sfs.array.linear_random(N, dx/2, 1.5*dx, center=acenter, orientation=anormal) -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_point( +d, selection, secondary_source = sfs.mono.wfs.point_25d( omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('linear_random_ps_wfs_25d_point') -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_plane( +d, selection, secondary_source = sfs.mono.wfs.plane_25d( omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('linear_random_ps_wfs_25d_plane') # rectangular array, secondary point sources array = sfs.array.rectangular((N, N//2), dx, center=acenter, orientation=anormal) -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_point( +d, selection, secondary_source = sfs.mono.wfs.point_25d( omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('rectangular_ps_wfs_25d_point') -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_plane( +d, selection, secondary_source = sfs.mono.wfs.plane_25d( omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('rectangular_ps_wfs_25d_plane') @@ -118,11 +118,11 @@ def compute_and_plot_soundfield(title): # circular array, secondary point sources N = 60 array = sfs.array.circular(N, 1, center=acenter) -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_point( +d, selection, secondary_source = sfs.mono.wfs.point_25d( omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('circular_ps_wfs_25d_point') -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_plane( +d, selection, secondary_source = sfs.mono.wfs.plane_25d( omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('circular_ps_wfs_25d_plane') @@ -132,7 +132,7 @@ def compute_and_plot_soundfield(title): xnorm = [0, 0, 0] talpha = 0 # switches off tapering -d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_2d_plane( +d, selection, secondary_source = sfs.mono.nfchoa.plane_2d( omega, array.x, 1, npw) compute_and_plot_soundfield('circular_ls_nfchoa_2d_plane') @@ -142,10 +142,10 @@ def compute_and_plot_soundfield(title): xnorm = [0, 0, 0] talpha = 0 # switches off tapering -d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_25d_point( +d, selection, secondary_source = sfs.mono.nfchoa.point_25d( omega, array.x, 1, xs) compute_and_plot_soundfield('circular_ps_nfchoa_25d_point') -d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_25d_plane( +d, selection, secondary_source = sfs.mono.nfchoa.plane_25d( omega, array.x, 1, npw) compute_and_plot_soundfield('circular_ps_nfchoa_25d_plane') diff --git a/doc/examples/sound_field_synthesis.py b/doc/examples/sound_field_synthesis.py index 733ca2b..81cd8be 100644 --- a/doc/examples/sound_field_synthesis.py +++ b/doc/examples/sound_field_synthesis.py @@ -44,22 +44,22 @@ # === compute driving function and determine active secondary sources === -#d, selection, secondary_source = sfs.mono.drivingfunction.delay_3d_plane(omega, array.x, array.n, npw) +#d, selection, secondary_source = sfs.mono.wfs.plane_3d_delay(omega, array.x, array.n, npw) -#d, selection, secondary_source = sfs.mono.drivingfunction.wfs_2d_line(omega, array.x, array.n, xs) +#d, selection, secondary_source = sfs.mono.wfs.line_2d(omega, array.x, array.n, xs) -#d, selection, secondary_source = sfs.mono.drivingfunction.wfs_2d_plane(omega, array.x, array.n, npw) -d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_plane(omega, array.x, array.n, npw, xref) -#d, selection, secondary_source = sfs.mono.drivingfunction.wfs_3d_plane(omega, array.x, array.n, npw) +#d, selection, secondary_source = sfs.mono.wfs.plane_2d(omega, array.x, array.n, npw) +d, selection, secondary_source = sfs.mono.wfs.plane_25d(omega, array.x, array.n, npw, xref) +#d, selection, secondary_source = sfs.mono.wfs.plane_3d(omega, array.x, array.n, npw) -#d, selection, secondary_source = sfs.mono.drivingfunction.wfs_2d_point(omega, array.x, array.n, xs) -#d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_point(omega, array.x, array.n, xs) -#d, selection, secondary_source = sfs.mono.drivingfunction.wfs_3d_point(omega, array.x, array.n, xs) +#d, selection, secondary_source = sfs.mono.wfs.point_2d(omega, array.x, array.n, xs) +#d, selection, secondary_source = sfs.mono.wfs.point_25d(omega, array.x, array.n, xs) +#d, selection, secondary_source = sfs.mono.wfs.point_3d(omega, array.x, array.n, xs) -#d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_2d_plane(omega, array.x, R, npw) +#d, selection, secondary_source = sfs.mono.nfchoa.plane_2d(omega, array.x, R, npw) -#d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_25d_point(omega, array.x, R, xs) -#d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_25d_plane(omega, array.x, R, npw) +#d, selection, secondary_source = sfs.mono.nfchoa.point_25d(omega, array.x, R, xs) +#d, selection, secondary_source = sfs.mono.nfchoa.plane_25d(omega, array.x, R, npw) # === compute tapering window === diff --git a/doc/examples/soundfigures.py b/doc/examples/soundfigures.py index e6a76a6..9405182 100644 --- a/doc/examples/soundfigures.py +++ b/doc/examples/soundfigures.py @@ -29,7 +29,7 @@ # driving function for sound figure figure = np.array(Image.open('figures/tree.png')) # read image from file figure = np.rot90(figure) # turn 0deg to the top -d, selection, secondary_source = sfs.mono.soundfigure.wfs_3d_pw( +d, selection, secondary_source = sfs.mono.wfs.soundfigure_3d( omega, array.x, array.n, figure, npw=npw) # compute synthesized sound field diff --git a/sfs/mono/__init__.py b/sfs/mono/__init__.py index e33502e..6961969 100644 --- a/sfs/mono/__init__.py +++ b/sfs/mono/__init__.py @@ -3,18 +3,17 @@ .. autosummary:: :toctree: - drivingfunction source - soundfigure -""" -import numpy as _np + wfs + nfchoa + sdm + esa -from . import drivingfunction +""" from . import source -from . import soundfigure - from .. import array as _array +import numpy as _np def shiftphase(p, phase): @@ -58,3 +57,27 @@ def synthesize(d, weights, ssd, secondary_source_function, **kwargs): if weight != 0: p += a * weight * d * secondary_source_function(x, n, **kwargs) return p + + +def secondary_source_point(omega, c): + """Create a point source for use in `sfs.mono.synthesize()`.""" + + def secondary_source(position, _, grid): + return source.point(omega, position, grid, c) + + return secondary_source + + +def secondary_source_line(omega, c): + """Create a line source for use in `sfs.mono.synthesize()`.""" + + def secondary_source(position, _, grid): + return source.line(omega, position, grid, c) + + return secondary_source + + +from . import esa +from . import nfchoa +from . import sdm +from . import wfs diff --git a/sfs/mono/drivingfunction.py b/sfs/mono/drivingfunction.py deleted file mode 100644 index 5dfb574..0000000 --- a/sfs/mono/drivingfunction.py +++ /dev/null @@ -1,1351 +0,0 @@ -"""Compute driving functions for various systems. - -.. include:: math-definitions.rst - -.. plot:: - :context: reset - - import matplotlib.pyplot as plt - import numpy as np - import sfs - - plt.rcParams['figure.figsize'] = 6, 6 - - xs = -1.5, 1.5, 0 - xs_focused = -0.5, 0.5, 0 - # normal vector for plane wave: - npw = sfs.util.direction_vector(np.radians(-45)) - # normal vector for focused source: - ns_focused = sfs.util.direction_vector(np.radians(-45)) - f = 300 # Hz - omega = 2 * np.pi * f - R = 1.5 # Radius of circular loudspeaker array - - grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02) - - array = sfs.array.circular(N=32, R=R) - - def plot(d, selection, secondary_source): - p = sfs.mono.synthesize(d, selection, array, secondary_source, grid=grid) - sfs.plot.soundfield(p, grid) - sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) - -""" - -import numpy as np -from numpy.core.umath_tests import inner1d # element-wise inner product -from scipy.special import jn, hankel2 -from .. import util -from .. import default -from . import source as _source - - -def wfs_2d_line(omega, x0, n0, xs, c=None): - r"""Driving function for 2-dimensional WFS for a virtual line source. - - Parameters - ---------- - omega : float - Angular frequency of line source. - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of normal vectors of secondary sources. - xs : (3,) array_like - Position of virtual line source. - c : float, optional - Speed of sound. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - .. math:: - - D(\x_0,\w) = \frac{\i}{2} \wc - \frac{\scalarprod{\x-\x_0}{\n_0}}{|\x-\x_\text{s}|} - \Hankel{2}{1}{\wc|\x-\x_\text{s}|} - - Examples - -------- - .. plot:: - :context: close-figs - - d, selection, secondary_source = sfs.mono.drivingfunction.wfs_2d_line( - omega, array.x, array.n, xs) - plot(d, selection, secondary_source) - - """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - k = util.wavenumber(omega, c) - ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - d = -1j/2 * k * inner1d(ds, n0) / r * hankel2(1, k * r) - selection = util.source_selection_line(n0, x0, xs) - return d, selection, secondary_source_line(omega, c) - - -def _wfs_point(omega, x0, n0, xs, c=None): - r"""Driving function for 2/3-dimensional WFS for a virtual point source. - - Parameters - ---------- - omega : float - Angular frequency of point source. - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of normal vectors of secondary sources. - xs : (3,) array_like - Position of virtual point source. - c : float, optional - Speed of sound. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - .. math:: - - D(\x_0, \w) = \i\wc \frac{\scalarprod{\x_0-\x_\text{s}}{\n_0}} - {|\x_0-\x_\text{s}|^{\frac{3}{2}}} - \e{-\i\wc |\x_0-\x_\text{s}|} - - Examples - -------- - .. plot:: - :context: close-figs - - d, selection, secondary_source = sfs.mono.drivingfunction.wfs_3d_point( - omega, array.x, array.n, xs) - plot(d, selection, secondary_source) - - """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - k = util.wavenumber(omega, c) - ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - d = 1j * k * inner1d(ds, n0) / r ** (3 / 2) * np.exp(-1j * k * r) - selection = util.source_selection_point(n0, x0, xs) - return d, selection, secondary_source_point(omega, c) - - -wfs_2d_point = _wfs_point - - -def wfs_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): - r"""Driving function for 2.5-dimensional WFS for a virtual point source. - - Parameters - ---------- - omega : float - Angular frequency of point source. - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of normal vectors of secondary sources. - xs : (3,) array_like - Position of virtual point source. - xref : (3,) array_like, optional - Reference point for synthesized sound field. - c : float, optional - Speed of sound. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - .. math:: - - D(\x_0,\w) = \sqrt{\i\wc |\x_\text{ref}-\x_0|} - \frac{\scalarprod{\x_0-\x_\text{s}}{\n_0}} - {|\x_0-\x_\text{s}|^\frac{3}{2}} - \e{-\i\wc |\x_0-\x_\text{s}|} - - Examples - -------- - .. plot:: - :context: close-figs - - d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_point( - omega, array.x, array.n, xs) - plot(d, selection, secondary_source) - - """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - xref = util.asarray_1d(xref) - k = util.wavenumber(omega, c) - ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - d = ( - wfs_25d_preeq(omega, omalias, c) * - np.sqrt(np.linalg.norm(xref - x0)) * inner1d(ds, n0) / - r ** (3 / 2) * np.exp(-1j * k * r)) - selection = util.source_selection_point(n0, x0, xs) - return d, selection, secondary_source_point(omega, c) - - -wfs_3d_point = _wfs_point - - -def _wfs_plane(omega, x0, n0, n=[0, 1, 0], c=None): - r"""Driving function for 2/3-dimensional WFS for a virtual plane wave. - - Parameters - ---------- - omega : float - Angular frequency of plane wave. - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of normal vectors of secondary sources. - n : (3,) array_like, optional - Normal vector (traveling direction) of plane wave. - c : float, optional - Speed of sound. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - Eq.(17) from :cite:`Spors2008`: - - .. math:: - - D(\x_0,\w) = \i\wc \scalarprod{\n}{\n_0} - \e{-\i\wc\scalarprod{\n}{\x_0}} - - Examples - -------- - .. plot:: - :context: close-figs - - d, selection, secondary_source = sfs.mono.drivingfunction.wfs_3d_plane( - omega, array.x, array.n, npw) - plot(d, selection, secondary_source) - - """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - n = util.normalize_vector(n) - k = util.wavenumber(omega, c) - d = 2j * k * np.inner(n, n0) * np.exp(-1j * k * np.inner(n, x0)) - selection = util.source_selection_plane(n0, n) - return d, selection, secondary_source_point(omega, c) - - -wfs_2d_plane = _wfs_plane - - -def wfs_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, - omalias=None): - r"""Driving function for 2.5-dimensional WFS for a virtual plane wave. - - Parameters - ---------- - omega : float - Angular frequency of plane wave. - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of normal vectors of secondary sources. - n : (3,) array_like, optional - Normal vector (traveling direction) of plane wave. - xref : (3,) array_like, optional - Reference point for synthesized sound field. - c : float, optional - Speed of sound. - omalias: float, optional - Angular frequency where spatial aliasing becomes prominent. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - .. math:: - - D_\text{2.5D}(\x_0,\w) = \sqrt{\i\wc |\x_\text{ref}-\x_0|} - \scalarprod{\n}{\n_0} - \e{-\i\wc \scalarprod{\n}{\x_0}} - - Examples - -------- - .. plot:: - :context: close-figs - - d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_plane( - omega, array.x, array.n, npw) - plot(d, selection, secondary_source) - - """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - n = util.normalize_vector(n) - xref = util.asarray_1d(xref) - k = util.wavenumber(omega, c) - d = ( - wfs_25d_preeq(omega, omalias, c) * - np.sqrt(8*np.pi * np.linalg.norm(xref - x0, axis=-1)) * - np.inner(n, n0) * np.exp(-1j * k * np.inner(n, x0))) - selection = util.source_selection_plane(n0, n) - return d, selection, secondary_source_point(omega, c) - - -wfs_3d_plane = _wfs_plane - - -def _wfs_focused(omega, x0, n0, xs, ns, c=None): - r"""Driving function for 2/3-dimensional WFS for a focused source. - - Parameters - ---------- - omega : float - Angular frequency of focused source. - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of normal vectors of secondary sources. - xs : (3,) array_like - Position of focused source. - ns : (3,) array_like - Direction of focused source. - c : float, optional - Speed of sound. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - .. math:: - - D(\x_0,\w) = \i\wc \frac{\scalarprod{\x_0-\x_\text{s}}{\n_0}} - {|\x_0-\x_\text{s}|^\frac{3}{2}} - \e{\i\wc |\x_0-\x_\text{s}|} - - Examples - -------- - .. plot:: - :context: close-figs - - d, selection, secondary_source = sfs.mono.drivingfunction.wfs_3d_focused( - omega, array.x, array.n, xs_focused, ns_focused) - plot(d, selection, secondary_source) - - """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - k = util.wavenumber(omega, c) - ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - d = 1j * k * inner1d(ds, n0) / r ** (3 / 2) * np.exp(1j * k * r) - selection = util.source_selection_focused(ns, x0, xs) - return d, selection, secondary_source_point(omega, c) - - -wfs_2d_focused = _wfs_focused - - -def wfs_25d_focused(omega, x0, n0, xs, ns, xref=[0, 0, 0], c=None, - omalias=None): - r"""Driving function for 2.5-dimensional WFS for a focused source. - - Parameters - ---------- - omega : float - Angular frequency of focused source. - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of normal vectors of secondary sources. - xs : (3,) array_like - Position of focused source. - ns : (3,) array_like - Direction of focused source. - xref : (3,) array_like, optional - Reference point for synthesized sound field. - c : float, optional - Speed of sound. - omalias: float, optional - Angular frequency where spatial aliasing becomes prominent. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - .. math:: - - D(\x_0,\w) = \sqrt{\i\wc |\x_\text{ref}-\x_0|} - \frac{\scalarprod{\x_0-\x_\text{s}}{\n_0}} - {|\x_0-\x_\text{s}|^\frac{3}{2}} - \e{\i\wc |\x_0-\x_\text{s}|} - - Examples - -------- - .. plot:: - :context: close-figs - - d, selection, secondary_source = sfs.mono.drivingfunction.wfs_25d_focused( - omega, array.x, array.n, xs_focused, ns_focused) - plot(d, selection, secondary_source) - - """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - xref = util.asarray_1d(xref) - k = util.wavenumber(omega, c) - ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - d = ( - wfs_25d_preeq(omega, omalias, c) * - np.sqrt(np.linalg.norm(xref - x0)) * inner1d(ds, n0) / - r ** (3 / 2) * np.exp(1j * k * r)) - selection = util.source_selection_focused(ns, x0, xs) - return d, selection, secondary_source_point(omega, c) - - -wfs_3d_focused = _wfs_focused - - -def wfs_25d_preeq(omega, omalias, c): - r"""Pre-equalization filter for 2.5-dimensional WFS. - - Parameters - ---------- - omega : float - Angular frequency. - omalias: float - Angular frequency where spatial aliasing becomes prominent. - c : float - Speed of sound. - - Returns - ------- - float - Complex weight for given angular frequency. - - Notes - ----- - .. math:: - - H(\w) = \begin{cases} - \sqrt{\i \wc} & \text{for } \w \leq \w_\text{alias} \\ - \sqrt{\i \frac{\w_\text{alias}}{c}} & \text{for } \w > \w_\text{alias} - \end{cases} - - """ - if omalias is None: - return np.sqrt(1j * util.wavenumber(omega, c)) - else: - if omega <= omalias: - return np.sqrt(1j * util.wavenumber(omega, c)) - else: - return np.sqrt(1j * util.wavenumber(omalias, c)) - - -def delay_3d_plane(omega, x0, n0, n=[0, 1, 0], c=None): - r"""Delay-only driving function for a virtual plane wave. - - Parameters - ---------- - omega : float - Angular frequency of plane wave. - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of normal vectors of secondary sources. - n : (3,) array_like, optional - Normal vector (traveling direction) of plane wave. - c : float, optional - Speed of sound. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - .. math:: - - D(\x_0,\w) = \e{-\i\wc\scalarprod{\n}{\x_0}} - - Examples - -------- - .. plot:: - :context: close-figs - - d, selection, secondary_source = sfs.mono.drivingfunction.delay_3d_plane( - omega, array.x, array.n, npw) - plot(d, selection, secondary_source) - - """ - x0 = util.asarray_of_rows(x0) - n = util.normalize_vector(n) - k = util.wavenumber(omega, c) - d = np.exp(-1j * k * np.inner(n, x0)) - selection = util.source_selection_plane(n0, n) - return d, selection, secondary_source_point(omega, c) - - -def nfchoa_2d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): - r"""Driving function for 2-dimensional NFC-HOA for a virtual plane wave. - - Parameters - ---------- - omega : float - Angular frequency of plane wave. - x0 : (N, 3) array_like - Sequence of secondary source positions. - r0 : float - Radius of circular secondary source distribution. - n : (3,) array_like, optional - Normal vector (traveling direction) of plane wave. - max_order : float, optional - Maximum order of circular harmonics used for the calculation. - c : float, optional - Speed of sound. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing only ``True`` indicating that - all secondary source are "active" for NFC-HOA. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - .. math:: - - D(\phi_0, \omega) = - -\frac{2\i}{\pi r_0} - \sum_{m=-M}^M - \frac{\i^{-m}}{\Hankel{2}{m}{\wc r_0}} - \e{\i m (\phi_0 - \phi_\text{pw})} - - See http://sfstoolbox.org/#equation-D.nfchoa.pw.2D. - - Examples - -------- - .. plot:: - :context: close-figs - - d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_2d_plane( - omega, array.x, R, npw) - plot(d, selection, secondary_source) - - """ - if max_order is None: - max_order = util.max_order_circular_harmonics(len(x0)) - - x0 = util.asarray_of_rows(x0) - k = util.wavenumber(omega, c) - n = util.normalize_vector(n) - phi, _, r = util.cart2sph(*n) - phi0 = util.cart2sph(*x0.T)[0] - d = 0 - for m in range(-max_order, max_order + 1): - d += 1j**-m / hankel2(m, k * r0) * np.exp(1j * m * (phi0 - phi)) - selection = util.source_selection_all(len(x0)) - return -2j / (np.pi*r0) * d, selection, secondary_source_point(omega, c) - - -def nfchoa_25d_point(omega, x0, r0, xs, max_order=None, c=None): - r"""Driving function for 2.5-dimensional NFC-HOA for a virtual point source. - - Parameters - ---------- - omega : float - Angular frequency of point source. - x0 : (N, 3) array_like - Sequence of secondary source positions. - r0 : float - Radius of circular secondary source distribution. - xs : (3,) array_like - Position of point source. - max_order : float, optional - Maximum order of circular harmonics used for the calculation. - c : float, optional - Speed of sound. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing only ``True`` indicating that - all secondary source are "active" for NFC-HOA. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - .. math:: - - D(\phi_0, \omega) = - \frac{1}{2 \pi r_0} - \sum_{m=-M}^M - \frac{\hankel{2}{|m|}{\wc r}}{\hankel{2}{|m|}{\wc r_0}} - \e{\i m (\phi_0 - \phi)} - - See http://sfstoolbox.org/#equation-D.nfchoa.ps.2.5D. - - Examples - -------- - .. plot:: - :context: close-figs - - d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_25d_point( - omega, array.x, R, xs) - plot(d, selection, secondary_source) - - """ - if max_order is None: - max_order = util.max_order_circular_harmonics(len(x0)) - - x0 = util.asarray_of_rows(x0) - k = util.wavenumber(omega, c) - xs = util.asarray_1d(xs) - phi, _, r = util.cart2sph(*xs) - phi0 = util.cart2sph(*x0.T)[0] - hr = util.spherical_hn2(range(0, max_order + 1), k * r) - hr0 = util.spherical_hn2(range(0, max_order + 1), k * r0) - d = 0 - for m in range(-max_order, max_order + 1): - d += hr[abs(m)] / hr0[abs(m)] * np.exp(1j * m * (phi0 - phi)) - selection = util.source_selection_all(len(x0)) - return d / (2 * np.pi * r0), selection, secondary_source_point(omega, c) - - -def nfchoa_25d_plane(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): - r"""Driving function for 2.5-dimensional NFC-HOA for a virtual plane wave. - - Parameters - ---------- - omega : float - Angular frequency of point source. - x0 : (N, 3) array_like - Sequence of secondary source positions. - r0 : float - Radius of circular secondary source distribution. - n : (3,) array_like, optional - Normal vector (traveling direction) of plane wave. - max_order : float, optional - Maximum order of circular harmonics used for the calculation. - c : float, optional - Speed of sound. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing only ``True`` indicating that - all secondary source are "active" for NFC-HOA. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - .. math:: - - D(\phi_0, \omega) = - \frac{2\i}{r_0} - \sum_{m=-M}^M - \frac{\i^{-|m|}}{\wc \hankel{2}{|m|}{\wc r_0}} - \e{\i m (\phi_0 - \phi_\text{pw})} - - See http://sfstoolbox.org/#equation-D.nfchoa.pw.2.5D. - - Examples - -------- - .. plot:: - :context: close-figs - - d, selection, secondary_source = sfs.mono.drivingfunction.nfchoa_25d_plane( - omega, array.x, R, npw) - plot(d, selection, secondary_source) - - """ - if max_order is None: - max_order = util.max_order_circular_harmonics(len(x0)) - - x0 = util.asarray_of_rows(x0) - k = util.wavenumber(omega, c) - n = util.normalize_vector(n) - phi, _, r = util.cart2sph(*n) - phi0 = util.cart2sph(*x0.T)[0] - d = 0 - hn2 = util.spherical_hn2(range(0, max_order + 1), k * r0) - for m in range(-max_order, max_order + 1): - d += (-1j)**abs(m) / (k * hn2[abs(m)]) * np.exp(1j * m * (phi0 - phi)) - selection = util.source_selection_all(len(x0)) - return 2*1j / r0 * d, selection, secondary_source_point(omega, c) - - -def sdm_2d_line(omega, x0, n0, xs, c=None): - r"""Driving function for 2-dimensional SDM for a virtual line source. - - Parameters - ---------- - omega : float - Angular frequency of line source. - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of normal vectors of secondary sources. - xs : (3,) array_like - Position of line source. - c : float, optional - Speed of sound. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - The secondary sources have to be located on the x-axis (y0=0). - Derived from :cite:`Spors2009`, Eq.(9), Eq.(4). - - Examples - -------- - .. plot:: - :context: close-figs - - array = sfs.array.linear(32, 0.2, orientation=[0, -1, 0]) - d, selection, secondary_source = sfs.mono.drivingfunction.sdm_2d_line( - omega, array.x, array.n, xs) - plot(d, selection, secondary_source) - - """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - k = util.wavenumber(omega, c) - ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - d = - 1j/2 * k * xs[1] / r * hankel2(1, k * r) - selection = util.source_selection_all(len(x0)) - return d, selection, secondary_source_line(omega, c) - - -def sdm_2d_plane(omega, x0, n0, n=[0, 1, 0], c=None): - r"""Driving function for 2-dimensional SDM for a virtual plane wave. - - Parameters - ---------- - omega : float - Angular frequency of plane wave. - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of normal vectors of secondary sources. - n: (3,) array_like, optional - Normal vector (traveling direction) of plane wave. - c : float, optional - Speed of sound. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - The secondary sources have to be located on the x-axis (y0=0). - Derived from :cite:`Ahrens2012`, Eq.(3.73), Eq.(C.5), Eq.(C.11): - - .. math:: - - D(\x_0,k) = k_\text{pw,y} \e{-\i k_\text{pw,x} x} - - Examples - -------- - .. plot:: - :context: close-figs - - array = sfs.array.linear(32, 0.2, orientation=[0, -1, 0]) - d, selection, secondary_source = sfs.mono.drivingfunction.sdm_2d_plane( - omega, array.x, array.n, npw) - plot(d, selection, secondary_source) - - """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - n = util.normalize_vector(n) - k = util.wavenumber(omega, c) - d = k * n[1] * np.exp(-1j * k * n[0] * x0[:, 0]) - selection = util.source_selection_all(len(x0)) - return d, selection, secondary_source_line(omega, c) - - -def sdm_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): - r"""Driving function for 2.5-dimensional SDM for a virtual plane wave. - - Parameters - ---------- - omega : float - Angular frequency of plane wave. - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of normal vectors of secondary sources. - n: (3,) array_like, optional - Normal vector (traveling direction) of plane wave. - xref : (3,) array_like, optional - Reference point for synthesized sound field. - c : float, optional - Speed of sound. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - The secondary sources have to be located on the x-axis (y0=0). - Eq.(3.79) from :cite:`Ahrens2012`. - - Examples - -------- - .. plot:: - :context: close-figs - - array = sfs.array.linear(32, 0.2, orientation=[0, -1, 0]) - d, selection, secondary_source = ( - sfs.mono.drivingfunction.sdm_25d_plane( - omega, array.x, array.n, npw, [0, -1, 0])) - plot(d, selection, secondary_source) - - """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - n = util.normalize_vector(n) - xref = util.asarray_1d(xref) - k = util.wavenumber(omega, c) - d = 4j * np.exp(-1j*k*n[1]*xref[1]) / hankel2(0, k*n[1]*xref[1]) * \ - np.exp(-1j*k*n[0]*x0[:, 0]) - selection = util.source_selection_all(len(x0)) - return d, selection, secondary_source_point(omega, c) - - -def sdm_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None): - r"""Driving function for 2.5-dimensional SDM for a virtual point source. - - Parameters - ---------- - omega : float - Angular frequency of point source. - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of normal vectors of secondary sources. - xs: (3,) array_like - Position of virtual point source. - xref : (3,) array_like, optional - Reference point for synthesized sound field. - c : float, optional - Speed of sound. - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - The secondary sources have to be located on the x-axis (y0=0). - Driving function from :cite:`Spors2010`, Eq.(24). - - Examples - -------- - .. plot:: - :context: close-figs - - array = sfs.array.linear(32, 0.2, orientation=[0, -1, 0]) - d, selection, secondary_source = ( - sfs.mono.drivingfunction.sdm_25d_point( - omega, array.x, array.n, xs, [0, -1, 0])) - plot(d, selection, secondary_source) - - """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - xref = util.asarray_1d(xref) - k = util.wavenumber(omega, c) - ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - d = 1/2 * 1j * k * np.sqrt(xref[1] / (xref[1] - xs[1])) * \ - xs[1] / r * hankel2(1, k * r) - selection = util.source_selection_all(len(x0)) - return d, selection, secondary_source_point(omega, c) - - -def esa_edge_2d_plane(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, - c=None): - r"""Driving function for 2-dimensional plane wave with edge ESA. - - Driving function for a virtual plane wave using the 2-dimensional ESA - for an edge-shaped secondary source distribution consisting of - monopole line sources. - - Parameters - ---------- - omega : float - Angular frequency. - x0 : int(N, 3) array_like - Sequence of secondary source positions. - n : (3,) array_like, optional - Normal vector of synthesized plane wave. - alpha : float, optional - Outer angle of edge. - Nc : int, optional - Number of elements for series expansion of driving function. Estimated - if not given. - c : float, optional - Speed of sound - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - One leg of the secondary sources has to be located on the x-axis (y0=0), - the edge at the origin. - - Derived from :cite:`Spors2016` - - """ - x0 = np.asarray(x0) - n = util.normalize_vector(n) - k = util.wavenumber(omega, c) - phi_s = np.arctan2(n[1], n[0]) + np.pi - L = x0.shape[0] - - r = np.linalg.norm(x0, axis=1) - phi = np.arctan2(x0[:, 1], x0[:, 0]) - phi = np.where(phi < 0, phi+2*np.pi, phi) - - if Nc is None: - Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) - - epsilon = np.ones(Nc) # weights for series expansion - epsilon[0] = 2 - - d = np.zeros(L, dtype=complex) - for m in np.arange(Nc): - nu = m*np.pi/alpha - d = d + 1/epsilon[m] * np.exp(1j*nu*np.pi/2) * np.sin(nu*phi_s) \ - * np.cos(nu*phi) * nu/r * jn(nu, k*r) - - d[phi > 0] = -d[phi > 0] - - selection = util.source_selection_all(len(x0)) - return 4*np.pi/alpha * d, selection, secondary_source_line(omega, c) - - -def esa_edge_dipole_2d_plane(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, - c=None): - r"""Driving function for 2-dimensional plane wave with edge dipole ESA. - - Driving function for a virtual plane wave using the 2-dimensional ESA - for an edge-shaped secondary source distribution consisting of - dipole line sources. - - Parameters - ---------- - omega : float - Angular frequency. - x0 : int(N, 3) array_like - Sequence of secondary source positions. - n : (3,) array_like, optional - Normal vector of synthesized plane wave. - alpha : float, optional - Outer angle of edge. - Nc : int, optional - Number of elements for series expansion of driving function. Estimated - if not given. - c : float, optional - Speed of sound - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - One leg of the secondary sources has to be located on the x-axis (y0=0), - the edge at the origin. - - Derived from :cite:`Spors2016` - - """ - x0 = np.asarray(x0) - n = util.normalize_vector(n) - k = util.wavenumber(omega, c) - phi_s = np.arctan2(n[1], n[0]) + np.pi - L = x0.shape[0] - - r = np.linalg.norm(x0, axis=1) - phi = np.arctan2(x0[:, 1], x0[:, 0]) - phi = np.where(phi < 0, phi+2*np.pi, phi) - - if Nc is None: - Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) - - epsilon = np.ones(Nc) # weights for series expansion - epsilon[0] = 2 - - d = np.zeros(L, dtype=complex) - for m in np.arange(Nc): - nu = m*np.pi/alpha - d = d + 1/epsilon[m] * np.exp(1j*nu*np.pi/2) * np.cos(nu*phi_s) \ - * np.cos(nu*phi) * jn(nu, k*r) - - return 4*np.pi/alpha * d - - -def esa_edge_2d_line(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): - r"""Driving function for 2-dimensional line source with edge ESA. - - Driving function for a virtual line source using the 2-dimensional ESA - for an edge-shaped secondary source distribution consisting of line - sources. - - Parameters - ---------- - omega : float - Angular frequency. - x0 : int(N, 3) array_like - Sequence of secondary source positions. - xs : (3,) array_like - Position of synthesized line source. - alpha : float, optional - Outer angle of edge. - Nc : int, optional - Number of elements for series expansion of driving function. Estimated - if not given. - c : float, optional - Speed of sound - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - One leg of the secondary sources has to be located on the x-axis (y0=0), - the edge at the origin. - - Derived from :cite:`Spors2016` - - """ - x0 = np.asarray(x0) - k = util.wavenumber(omega, c) - phi_s = np.arctan2(xs[1], xs[0]) - if phi_s < 0: - phi_s = phi_s + 2*np.pi - r_s = np.linalg.norm(xs) - L = x0.shape[0] - - r = np.linalg.norm(x0, axis=1) - phi = np.arctan2(x0[:, 1], x0[:, 0]) - phi = np.where(phi < 0, phi+2*np.pi, phi) - - if Nc is None: - Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) - - epsilon = np.ones(Nc) # weights for series expansion - epsilon[0] = 2 - - d = np.zeros(L, dtype=complex) - idx = (r <= r_s) - for m in np.arange(Nc): - nu = m*np.pi/alpha - f = 1/epsilon[m] * np.sin(nu*phi_s) * np.cos(nu*phi) * nu/r - d[idx] = d[idx] + f[idx] * jn(nu, k*r[idx]) * hankel2(nu, k*r_s) - d[~idx] = d[~idx] + f[~idx] * jn(nu, k*r_s) * hankel2(nu, k*r[~idx]) - - d[phi > 0] = -d[phi > 0] - - selection = util.source_selection_all(len(x0)) - return -1j*np.pi/alpha * d, selection, secondary_source_line(omega, c) - - -def esa_edge_dipole_2d_line(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): - r"""Driving function for 2-dimensional line source with edge dipole ESA. - - Driving function for a virtual line source using the 2-dimensional ESA - for an edge-shaped secondary source distribution consisting of dipole line - sources. - - Parameters - ---------- - omega : float - Angular frequency. - x0 : (N, 3) array_like - Sequence of secondary source positions. - xs : (3,) array_like - Position of synthesized line source. - alpha : float, optional - Outer angle of edge. - Nc : int, optional - Number of elements for series expansion of driving function. Estimated - if not given. - c : float, optional - Speed of sound - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - One leg of the secondary sources has to be located on the x-axis (y0=0), - the edge at the origin. - - Derived from :cite:`Spors2016` - - """ - x0 = np.asarray(x0) - k = util.wavenumber(omega, c) - phi_s = np.arctan2(xs[1], xs[0]) - if phi_s < 0: - phi_s = phi_s + 2*np.pi - r_s = np.linalg.norm(xs) - L = x0.shape[0] - - r = np.linalg.norm(x0, axis=1) - phi = np.arctan2(x0[:, 1], x0[:, 0]) - phi = np.where(phi < 0, phi+2*np.pi, phi) - - if Nc is None: - Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) - - epsilon = np.ones(Nc) # weights for series expansion - epsilon[0] = 2 - - d = np.zeros(L, dtype=complex) - idx = (r <= r_s) - for m in np.arange(Nc): - nu = m*np.pi/alpha - f = 1/epsilon[m] * np.cos(nu*phi_s) * np.cos(nu*phi) - d[idx] = d[idx] + f[idx] * jn(nu, k*r[idx]) * hankel2(nu, k*r_s) - d[~idx] = d[~idx] + f[~idx] * jn(nu, k*r_s) * hankel2(nu, k*r[~idx]) - - return -1j*np.pi/alpha * d - - -def esa_edge_25d_point(omega, x0, xs, xref=[2, -2, 0], alpha=3/2*np.pi, - Nc=None, c=None): - r"""Driving function for 2.5-dimensional point source with edge ESA. - - Driving function for a virtual point source using the 2.5-dimensional - ESA for an edge-shaped secondary source distribution consisting of point - sources. - - Parameters - ---------- - omega : float - Angular frequency. - x0 : int(N, 3) array_like - Sequence of secondary source positions. - xs : (3,) array_like - Position of synthesized line source. - xref: (3,) array_like or float - Reference position or reference distance - alpha : float, optional - Outer angle of edge. - Nc : int, optional - Number of elements for series expansion of driving function. Estimated - if not given. - c : float, optional - Speed of sound - - Returns - ------- - d : (N,) numpy.ndarray - Complex weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. - - Notes - ----- - One leg of the secondary sources has to be located on the x-axis (y0=0), - the edge at the origin. - - Derived from :cite:`Spors2016` - - """ - x0 = np.asarray(x0) - xs = np.asarray(xs) - xref = np.asarray(xref) - - if np.isscalar(xref): - a = np.linalg.norm(xref)/np.linalg.norm(xref-xs) - else: - a = np.linalg.norm(xref-x0, axis=1)/np.linalg.norm(xref-xs) - - d, selection, _ = esa_edge_2d_line(omega, x0, xs, alpha=alpha, Nc=Nc, c=c) - return 1j*np.sqrt(a) * d, selection, secondary_source_point(omega, c) - - -def secondary_source_point(omega, c): - """Create a point source for use in `sfs.mono.synthesize()`.""" - - def secondary_source(position, _, grid): - return _source.point(omega, position, grid, c) - - return secondary_source - - -def secondary_source_line(omega, c): - """Create a line source for use in `sfs.mono.synthesize()`.""" - - def secondary_source(position, _, grid): - return _source.line(omega, position, grid, c) - - return secondary_source diff --git a/sfs/mono/esa.py b/sfs/mono/esa.py new file mode 100644 index 0000000..5584761 --- /dev/null +++ b/sfs/mono/esa.py @@ -0,0 +1,358 @@ +"""Compute ESA driving functions for various systems. + +ESA is abbreviation for equivalent scattering approach. + +ESA driving functions for an edge-shaped SSD are provided below. +Further ESA for different geometries might be added here. + +Note that mode-matching (such as NFC-HOA, SDM) are equivalent +to ESA in their specific geometries (spherical/circular, planar/linear). + +""" + +import numpy as np +from scipy.special import jn, hankel2 +from .. import util +from . import secondary_source_line, secondary_source_point + + +def plane_2d_edge(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, + c=None): + r"""Driving function for 2-dimensional plane wave with edge ESA. + + Driving function for a virtual plane wave using the 2-dimensional ESA + for an edge-shaped secondary source distribution consisting of + monopole line sources. + + Parameters + ---------- + omega : float + Angular frequency. + x0 : int(N, 3) array_like + Sequence of secondary source positions. + n : (3,) array_like, optional + Normal vector of synthesized plane wave. + alpha : float, optional + Outer angle of edge. + Nc : int, optional + Number of elements for series expansion of driving function. Estimated + if not given. + c : float, optional + Speed of sound + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + One leg of the secondary sources has to be located on the x-axis (y0=0), + the edge at the origin. + + Derived from :cite:`Spors2016` + + """ + x0 = np.asarray(x0) + n = util.normalize_vector(n) + k = util.wavenumber(omega, c) + phi_s = np.arctan2(n[1], n[0]) + np.pi + L = x0.shape[0] + + r = np.linalg.norm(x0, axis=1) + phi = np.arctan2(x0[:, 1], x0[:, 0]) + phi = np.where(phi < 0, phi+2*np.pi, phi) + + if Nc is None: + Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) + + epsilon = np.ones(Nc) # weights for series expansion + epsilon[0] = 2 + + d = np.zeros(L, dtype=complex) + for m in np.arange(Nc): + nu = m*np.pi/alpha + d = d + 1/epsilon[m] * np.exp(1j*nu*np.pi/2) * np.sin(nu*phi_s) \ + * np.cos(nu*phi) * nu/r * jn(nu, k*r) + + d[phi > 0] = -d[phi > 0] + + selection = util.source_selection_all(len(x0)) + return 4*np.pi/alpha * d, selection, secondary_source_line(omega, c) + + +def plane_2d_edge_dipole_ssd(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, + c=None): + r"""Driving function for 2-dimensional plane wave with edge dipole ESA. + + Driving function for a virtual plane wave using the 2-dimensional ESA + for an edge-shaped secondary source distribution consisting of + dipole line sources. + + Parameters + ---------- + omega : float + Angular frequency. + x0 : int(N, 3) array_like + Sequence of secondary source positions. + n : (3,) array_like, optional + Normal vector of synthesized plane wave. + alpha : float, optional + Outer angle of edge. + Nc : int, optional + Number of elements for series expansion of driving function. Estimated + if not given. + c : float, optional + Speed of sound + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + One leg of the secondary sources has to be located on the x-axis (y0=0), + the edge at the origin. + + Derived from :cite:`Spors2016` + + """ + x0 = np.asarray(x0) + n = util.normalize_vector(n) + k = util.wavenumber(omega, c) + phi_s = np.arctan2(n[1], n[0]) + np.pi + L = x0.shape[0] + + r = np.linalg.norm(x0, axis=1) + phi = np.arctan2(x0[:, 1], x0[:, 0]) + phi = np.where(phi < 0, phi+2*np.pi, phi) + + if Nc is None: + Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) + + epsilon = np.ones(Nc) # weights for series expansion + epsilon[0] = 2 + + d = np.zeros(L, dtype=complex) + for m in np.arange(Nc): + nu = m*np.pi/alpha + d = d + 1/epsilon[m] * np.exp(1j*nu*np.pi/2) * np.cos(nu*phi_s) \ + * np.cos(nu*phi) * jn(nu, k*r) + + return 4*np.pi/alpha * d + + +def line_2d_edge(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): + r"""Driving function for 2-dimensional line source with edge ESA. + + Driving function for a virtual line source using the 2-dimensional ESA + for an edge-shaped secondary source distribution consisting of line + sources. + + Parameters + ---------- + omega : float + Angular frequency. + x0 : int(N, 3) array_like + Sequence of secondary source positions. + xs : (3,) array_like + Position of synthesized line source. + alpha : float, optional + Outer angle of edge. + Nc : int, optional + Number of elements for series expansion of driving function. Estimated + if not given. + c : float, optional + Speed of sound + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + One leg of the secondary sources has to be located on the x-axis (y0=0), + the edge at the origin. + + Derived from :cite:`Spors2016` + + """ + x0 = np.asarray(x0) + k = util.wavenumber(omega, c) + phi_s = np.arctan2(xs[1], xs[0]) + if phi_s < 0: + phi_s = phi_s + 2*np.pi + r_s = np.linalg.norm(xs) + L = x0.shape[0] + + r = np.linalg.norm(x0, axis=1) + phi = np.arctan2(x0[:, 1], x0[:, 0]) + phi = np.where(phi < 0, phi+2*np.pi, phi) + + if Nc is None: + Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) + + epsilon = np.ones(Nc) # weights for series expansion + epsilon[0] = 2 + + d = np.zeros(L, dtype=complex) + idx = (r <= r_s) + for m in np.arange(Nc): + nu = m*np.pi/alpha + f = 1/epsilon[m] * np.sin(nu*phi_s) * np.cos(nu*phi) * nu/r + d[idx] = d[idx] + f[idx] * jn(nu, k*r[idx]) * hankel2(nu, k*r_s) + d[~idx] = d[~idx] + f[~idx] * jn(nu, k*r_s) * hankel2(nu, k*r[~idx]) + + d[phi > 0] = -d[phi > 0] + + selection = util.source_selection_all(len(x0)) + return -1j*np.pi/alpha * d, selection, secondary_source_line(omega, c) + + +def line_2d_edge_dipole_ssd(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): + r"""Driving function for 2-dimensional line source with edge dipole ESA. + + Driving function for a virtual line source using the 2-dimensional ESA + for an edge-shaped secondary source distribution consisting of dipole line + sources. + + Parameters + ---------- + omega : float + Angular frequency. + x0 : (N, 3) array_like + Sequence of secondary source positions. + xs : (3,) array_like + Position of synthesized line source. + alpha : float, optional + Outer angle of edge. + Nc : int, optional + Number of elements for series expansion of driving function. Estimated + if not given. + c : float, optional + Speed of sound + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + One leg of the secondary sources has to be located on the x-axis (y0=0), + the edge at the origin. + + Derived from :cite:`Spors2016` + + """ + x0 = np.asarray(x0) + k = util.wavenumber(omega, c) + phi_s = np.arctan2(xs[1], xs[0]) + if phi_s < 0: + phi_s = phi_s + 2*np.pi + r_s = np.linalg.norm(xs) + L = x0.shape[0] + + r = np.linalg.norm(x0, axis=1) + phi = np.arctan2(x0[:, 1], x0[:, 0]) + phi = np.where(phi < 0, phi+2*np.pi, phi) + + if Nc is None: + Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) + + epsilon = np.ones(Nc) # weights for series expansion + epsilon[0] = 2 + + d = np.zeros(L, dtype=complex) + idx = (r <= r_s) + for m in np.arange(Nc): + nu = m*np.pi/alpha + f = 1/epsilon[m] * np.cos(nu*phi_s) * np.cos(nu*phi) + d[idx] = d[idx] + f[idx] * jn(nu, k*r[idx]) * hankel2(nu, k*r_s) + d[~idx] = d[~idx] + f[~idx] * jn(nu, k*r_s) * hankel2(nu, k*r[~idx]) + + return -1j*np.pi/alpha * d + + +def point_25d_edge(omega, x0, xs, xref=[2, -2, 0], alpha=3/2*np.pi, + Nc=None, c=None): + r"""Driving function for 2.5-dimensional point source with edge ESA. + + Driving function for a virtual point source using the 2.5-dimensional + ESA for an edge-shaped secondary source distribution consisting of point + sources. + + Parameters + ---------- + omega : float + Angular frequency. + x0 : int(N, 3) array_like + Sequence of secondary source positions. + xs : (3,) array_like + Position of synthesized line source. + xref: (3,) array_like or float + Reference position or reference distance + alpha : float, optional + Outer angle of edge. + Nc : int, optional + Number of elements for series expansion of driving function. Estimated + if not given. + c : float, optional + Speed of sound + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + One leg of the secondary sources has to be located on the x-axis (y0=0), + the edge at the origin. + + Derived from :cite:`Spors2016` + + """ + x0 = np.asarray(x0) + xs = np.asarray(xs) + xref = np.asarray(xref) + + if np.isscalar(xref): + a = np.linalg.norm(xref)/np.linalg.norm(xref-xs) + else: + a = np.linalg.norm(xref-x0, axis=1)/np.linalg.norm(xref-xs) + + d, selection, _ = line_2d_edge(omega, x0, xs, alpha=alpha, Nc=Nc, c=c) + return 1j*np.sqrt(a) * d, selection, secondary_source_point(omega, c) diff --git a/sfs/mono/nfchoa.py b/sfs/mono/nfchoa.py new file mode 100644 index 0000000..6677d22 --- /dev/null +++ b/sfs/mono/nfchoa.py @@ -0,0 +1,236 @@ +"""Compute NFC-HOA driving functions. + +.. include:: math-definitions.rst + +.. plot:: + :context: reset + + import matplotlib.pyplot as plt + import numpy as np + import sfs + + plt.rcParams['figure.figsize'] = 6, 6 + + xs = -1.5, 1.5, 0 + # normal vector for plane wave: + npw = sfs.util.direction_vector(np.radians(-45)) + f = 300 # Hz + omega = 2 * np.pi * f + R = 1.5 # Radius of circular loudspeaker array + + grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02) + + array = sfs.array.circular(N=32, R=R) + + def plot(d, selection, secondary_source): + p = sfs.mono.synthesize(d, selection, array, secondary_source, grid=grid) + sfs.plot.soundfield(p, grid) + sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) + +""" + +import numpy as np +from scipy.special import hankel2 +from .. import util +from . import secondary_source_point + + +def plane_2d(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): + r"""Driving function for 2-dimensional NFC-HOA for a virtual plane wave. + + Parameters + ---------- + omega : float + Angular frequency of plane wave. + x0 : (N, 3) array_like + Sequence of secondary source positions. + r0 : float + Radius of circular secondary source distribution. + n : (3,) array_like, optional + Normal vector (traveling direction) of plane wave. + max_order : float, optional + Maximum order of circular harmonics used for the calculation. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing only ``True`` indicating that + all secondary source are "active" for NFC-HOA. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + .. math:: + + D(\phi_0, \omega) = + -\frac{2\i}{\pi r_0} + \sum_{m=-M}^M + \frac{\i^{-m}}{\Hankel{2}{m}{\wc r_0}} + \e{\i m (\phi_0 - \phi_\text{pw})} + + See http://sfstoolbox.org/#equation-D.nfchoa.pw.2D. + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.nfchoa.plane_2d( + omega, array.x, R, npw) + plot(d, selection, secondary_source) + + """ + if max_order is None: + max_order = util.max_order_circular_harmonics(len(x0)) + + x0 = util.asarray_of_rows(x0) + k = util.wavenumber(omega, c) + n = util.normalize_vector(n) + phi, _, r = util.cart2sph(*n) + phi0 = util.cart2sph(*x0.T)[0] + d = 0 + for m in range(-max_order, max_order + 1): + d += 1j**-m / hankel2(m, k * r0) * np.exp(1j * m * (phi0 - phi)) + selection = util.source_selection_all(len(x0)) + return -2j / (np.pi*r0) * d, selection, secondary_source_point(omega, c) + + +def point_25d(omega, x0, r0, xs, max_order=None, c=None): + r"""Driving function for 2.5-dimensional NFC-HOA for a virtual point source. + + Parameters + ---------- + omega : float + Angular frequency of point source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + r0 : float + Radius of circular secondary source distribution. + xs : (3,) array_like + Position of point source. + max_order : float, optional + Maximum order of circular harmonics used for the calculation. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing only ``True`` indicating that + all secondary source are "active" for NFC-HOA. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + .. math:: + + D(\phi_0, \omega) = + \frac{1}{2 \pi r_0} + \sum_{m=-M}^M + \frac{\hankel{2}{|m|}{\wc r}}{\hankel{2}{|m|}{\wc r_0}} + \e{\i m (\phi_0 - \phi)} + + See http://sfstoolbox.org/#equation-D.nfchoa.ps.2.5D. + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.nfchoa.point_25d( + omega, array.x, R, xs) + plot(d, selection, secondary_source) + + """ + if max_order is None: + max_order = util.max_order_circular_harmonics(len(x0)) + + x0 = util.asarray_of_rows(x0) + k = util.wavenumber(omega, c) + xs = util.asarray_1d(xs) + phi, _, r = util.cart2sph(*xs) + phi0 = util.cart2sph(*x0.T)[0] + hr = util.spherical_hn2(range(0, max_order + 1), k * r) + hr0 = util.spherical_hn2(range(0, max_order + 1), k * r0) + d = 0 + for m in range(-max_order, max_order + 1): + d += hr[abs(m)] / hr0[abs(m)] * np.exp(1j * m * (phi0 - phi)) + selection = util.source_selection_all(len(x0)) + return d / (2 * np.pi * r0), selection, secondary_source_point(omega, c) + + +def plane_25d(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): + r"""Driving function for 2.5-dimensional NFC-HOA for a virtual plane wave. + + Parameters + ---------- + omega : float + Angular frequency of point source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + r0 : float + Radius of circular secondary source distribution. + n : (3,) array_like, optional + Normal vector (traveling direction) of plane wave. + max_order : float, optional + Maximum order of circular harmonics used for the calculation. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing only ``True`` indicating that + all secondary source are "active" for NFC-HOA. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + .. math:: + + D(\phi_0, \omega) = + \frac{2\i}{r_0} + \sum_{m=-M}^M + \frac{\i^{-|m|}}{\wc \hankel{2}{|m|}{\wc r_0}} + \e{\i m (\phi_0 - \phi_\text{pw})} + + See http://sfstoolbox.org/#equation-D.nfchoa.pw.2.5D. + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.nfchoa.plane_25d( + omega, array.x, R, npw) + plot(d, selection, secondary_source) + + """ + if max_order is None: + max_order = util.max_order_circular_harmonics(len(x0)) + + x0 = util.asarray_of_rows(x0) + k = util.wavenumber(omega, c) + n = util.normalize_vector(n) + phi, _, r = util.cart2sph(*n) + phi0 = util.cart2sph(*x0.T)[0] + d = 0 + hn2 = util.spherical_hn2(range(0, max_order + 1), k * r0) + for m in range(-max_order, max_order + 1): + d += (-1j)**abs(m) / (k * hn2[abs(m)]) * np.exp(1j * m * (phi0 - phi)) + selection = util.source_selection_all(len(x0)) + return 2*1j / r0 * d, selection, secondary_source_point(omega, c) diff --git a/sfs/mono/sdm.py b/sfs/mono/sdm.py new file mode 100644 index 0000000..2d775ee --- /dev/null +++ b/sfs/mono/sdm.py @@ -0,0 +1,254 @@ +"""Compute SDM driving functions. + +.. include:: math-definitions.rst + +.. plot:: + :context: reset + + import matplotlib.pyplot as plt + import numpy as np + import sfs + + plt.rcParams['figure.figsize'] = 6, 6 + + xs = -1.5, 1.5, 0 + # normal vector for plane wave: + npw = sfs.util.direction_vector(np.radians(-45)) + f = 300 # Hz + omega = 2 * np.pi * f + + grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02) + + array = sfs.array.linear(32, 0.2, orientation=[0, -1, 0]) + + def plot(d, selection, secondary_source): + p = sfs.mono.synthesize(d, selection, array, secondary_source, grid=grid) + sfs.plot.soundfield(p, grid) + sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) + +""" + +import numpy as np +from scipy.special import hankel2 +from .. import util +from . import secondary_source_line, secondary_source_point + + +def line_2d(omega, x0, n0, xs, c=None): + r"""Driving function for 2-dimensional SDM for a virtual line source. + + Parameters + ---------- + omega : float + Angular frequency of line source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs : (3,) array_like + Position of line source. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + The secondary sources have to be located on the x-axis (y0=0). + Derived from :cite:`Spors2009`, Eq.(9), Eq.(4). + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.sdm.line_2d( + omega, array.x, array.n, xs) + plot(d, selection, secondary_source) + + """ + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + xs = util.asarray_1d(xs) + k = util.wavenumber(omega, c) + ds = x0 - xs + r = np.linalg.norm(ds, axis=1) + d = - 1j/2 * k * xs[1] / r * hankel2(1, k * r) + selection = util.source_selection_all(len(x0)) + return d, selection, secondary_source_line(omega, c) + + +def plane_2d(omega, x0, n0, n=[0, 1, 0], c=None): + r"""Driving function for 2-dimensional SDM for a virtual plane wave. + + Parameters + ---------- + omega : float + Angular frequency of plane wave. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + n: (3,) array_like, optional + Normal vector (traveling direction) of plane wave. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + The secondary sources have to be located on the x-axis (y0=0). + Derived from :cite:`Ahrens2012`, Eq.(3.73), Eq.(C.5), Eq.(C.11): + + .. math:: + + D(\x_0,k) = k_\text{pw,y} \e{-\i k_\text{pw,x} x} + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.sdm.plane_2d( + omega, array.x, array.n, npw) + plot(d, selection, secondary_source) + + """ + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + n = util.normalize_vector(n) + k = util.wavenumber(omega, c) + d = k * n[1] * np.exp(-1j * k * n[0] * x0[:, 0]) + selection = util.source_selection_all(len(x0)) + return d, selection, secondary_source_line(omega, c) + + +def plane_25d(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): + r"""Driving function for 2.5-dimensional SDM for a virtual plane wave. + + Parameters + ---------- + omega : float + Angular frequency of plane wave. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + n: (3,) array_like, optional + Normal vector (traveling direction) of plane wave. + xref : (3,) array_like, optional + Reference point for synthesized sound field. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + The secondary sources have to be located on the x-axis (y0=0). + Eq.(3.79) from :cite:`Ahrens2012`. + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.sdm.plane_25d( + omega, array.x, array.n, npw, [0, -1, 0]) + plot(d, selection, secondary_source) + + """ + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + n = util.normalize_vector(n) + xref = util.asarray_1d(xref) + k = util.wavenumber(omega, c) + d = 4j * np.exp(-1j*k*n[1]*xref[1]) / hankel2(0, k*n[1]*xref[1]) * \ + np.exp(-1j*k*n[0]*x0[:, 0]) + selection = util.source_selection_all(len(x0)) + return d, selection, secondary_source_point(omega, c) + + +def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None): + r"""Driving function for 2.5-dimensional SDM for a virtual point source. + + Parameters + ---------- + omega : float + Angular frequency of point source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs: (3,) array_like + Position of virtual point source. + xref : (3,) array_like, optional + Reference point for synthesized sound field. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + The secondary sources have to be located on the x-axis (y0=0). + Driving function from :cite:`Spors2010`, Eq.(24). + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.sdm.point_25d( + omega, array.x, array.n, xs, [0, -1, 0]) + plot(d, selection, secondary_source) + + """ + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + xs = util.asarray_1d(xs) + xref = util.asarray_1d(xref) + k = util.wavenumber(omega, c) + ds = x0 - xs + r = np.linalg.norm(ds, axis=1) + d = 1/2 * 1j * k * np.sqrt(xref[1] / (xref[1] - xs[1])) * \ + xs[1] / r * hankel2(1, k * r) + selection = util.source_selection_all(len(x0)) + return d, selection, secondary_source_point(omega, c) diff --git a/sfs/mono/soundfigure.py b/sfs/mono/soundfigure.py deleted file mode 100644 index fcaf4f5..0000000 --- a/sfs/mono/soundfigure.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Compute driving functions for sound figures.""" - -import numpy as np -from .. import util -from .drivingfunction import wfs_3d_plane - - -def wfs_3d_pw(omega, x0, n0, figure, npw=[0, 0, 1], c=None): - """Compute driving function for a 2D sound figure. - - Based on - [Helwani et al., The Synthesis of Sound Figures, MSSP, 2013] - - """ - x0 = np.asarray(x0) - n0 = np.asarray(n0) - k = util.wavenumber(omega, c) - nx, ny = figure.shape - - # 2D spatial DFT of image - figure = np.fft.fftshift(figure, axes=(0, 1)) # sign of spatial DFT - figure = np.fft.fft2(figure) - # wavenumbers - kx = np.fft.fftfreq(nx, 1./nx) - ky = np.fft.fftfreq(ny, 1./ny) - # shift spectrum due to desired plane wave - figure = np.roll(figure, int(k*npw[0]), axis=0) - figure = np.roll(figure, int(k*npw[1]), axis=1) - # search and iterate over propagating plane wave components - kxx, kyy = np.meshgrid(kx, ky, sparse=True) - rho = np.sqrt((kxx) ** 2 + (kyy) ** 2) - d = 0 - for n in range(nx): - for m in range(ny): - if(rho[n, m] < k): - # dispertion relation - kz = np.sqrt(k**2 - rho[n, m]**2) - # normal vector of plane wave - npw = 1/k * np.asarray([kx[n], ky[m], kz]) - npw = npw / np.linalg.norm(npw) - # driving function of plane wave with positive kz - d_component, selection, secondary_source = wfs_3d_plane( - omega, x0, n0, npw, c) - d += selection * figure[n, m] * d_component - - return d, util.source_selection_all(len(d)), secondary_source diff --git a/sfs/mono/wfs.py b/sfs/mono/wfs.py new file mode 100644 index 0000000..a05aa0f --- /dev/null +++ b/sfs/mono/wfs.py @@ -0,0 +1,604 @@ +"""Compute WFS driving functions. + +.. include:: math-definitions.rst + +.. plot:: + :context: reset + + import matplotlib.pyplot as plt + import numpy as np + import sfs + + plt.rcParams['figure.figsize'] = 6, 6 + + xs = -1.5, 1.5, 0 + xs_focused = -0.5, 0.5, 0 + # normal vector for plane wave: + npw = sfs.util.direction_vector(np.radians(-45)) + # normal vector for focused source: + ns_focused = sfs.util.direction_vector(np.radians(-45)) + f = 300 # Hz + omega = 2 * np.pi * f + R = 1.5 # Radius of circular loudspeaker array + + grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02) + + array = sfs.array.circular(N=32, R=R) + + def plot(d, selection, secondary_source): + p = sfs.mono.synthesize(d, selection, array, secondary_source, grid=grid) + sfs.plot.soundfield(p, grid) + sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) + +""" + +import numpy as np +from numpy.core.umath_tests import inner1d # element-wise inner product +from scipy.special import hankel2 +from .. import util +from . import secondary_source_line, secondary_source_point + + +def line_2d(omega, x0, n0, xs, c=None): + r"""Driving function for 2-dimensional WFS for a virtual line source. + + Parameters + ---------- + omega : float + Angular frequency of line source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs : (3,) array_like + Position of virtual line source. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + .. math:: + + D(\x_0,\w) = \frac{\i}{2} \wc + \frac{\scalarprod{\x-\x_0}{\n_0}}{|\x-\x_\text{s}|} + \Hankel{2}{1}{\wc|\x-\x_\text{s}|} + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.wfs.line_2d( + omega, array.x, array.n, xs) + plot(d, selection, secondary_source) + + """ + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + xs = util.asarray_1d(xs) + k = util.wavenumber(omega, c) + ds = x0 - xs + r = np.linalg.norm(ds, axis=1) + d = -1j/2 * k * inner1d(ds, n0) / r * hankel2(1, k * r) + selection = util.source_selection_line(n0, x0, xs) + return d, selection, secondary_source_line(omega, c) + + +def _point(omega, x0, n0, xs, c=None): + r"""Driving function for 2/3-dimensional WFS for a virtual point source. + + Parameters + ---------- + omega : float + Angular frequency of point source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs : (3,) array_like + Position of virtual point source. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + .. math:: + + D(\x_0, \w) = \i\wc \frac{\scalarprod{\x_0-\x_\text{s}}{\n_0}} + {|\x_0-\x_\text{s}|^{\frac{3}{2}}} + \e{-\i\wc |\x_0-\x_\text{s}|} + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.wfs.point_3d( + omega, array.x, array.n, xs) + plot(d, selection, secondary_source) + + """ + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + xs = util.asarray_1d(xs) + k = util.wavenumber(omega, c) + ds = x0 - xs + r = np.linalg.norm(ds, axis=1) + d = 1j * k * inner1d(ds, n0) / r ** (3 / 2) * np.exp(-1j * k * r) + selection = util.source_selection_point(n0, x0, xs) + return d, selection, secondary_source_point(omega, c) + + +point_2d = _point + + +def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): + r"""Driving function for 2.5-dimensional WFS for a virtual point source. + + Parameters + ---------- + omega : float + Angular frequency of point source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs : (3,) array_like + Position of virtual point source. + xref : (3,) array_like, optional + Reference point for synthesized sound field. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + .. math:: + + D(\x_0,\w) = \sqrt{\i\wc |\x_\text{ref}-\x_0|} + \frac{\scalarprod{\x_0-\x_\text{s}}{\n_0}} + {|\x_0-\x_\text{s}|^\frac{3}{2}} + \e{-\i\wc |\x_0-\x_\text{s}|} + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.wfs.point_25d( + omega, array.x, array.n, xs) + plot(d, selection, secondary_source) + + """ + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + xs = util.asarray_1d(xs) + xref = util.asarray_1d(xref) + k = util.wavenumber(omega, c) + ds = x0 - xs + r = np.linalg.norm(ds, axis=1) + d = ( + preeq_25d(omega, omalias, c) * + np.sqrt(np.linalg.norm(xref - x0)) * inner1d(ds, n0) / + r ** (3 / 2) * np.exp(-1j * k * r)) + selection = util.source_selection_point(n0, x0, xs) + return d, selection, secondary_source_point(omega, c) + + +point_3d = _point + + +def _plane(omega, x0, n0, n=[0, 1, 0], c=None): + r"""Driving function for 2/3-dimensional WFS for a virtual plane wave. + + Parameters + ---------- + omega : float + Angular frequency of plane wave. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + n : (3,) array_like, optional + Normal vector (traveling direction) of plane wave. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + Eq.(17) from :cite:`Spors2008`: + + .. math:: + + D(\x_0,\w) = \i\wc \scalarprod{\n}{\n_0} + \e{-\i\wc\scalarprod{\n}{\x_0}} + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.wfs.plane_3d( + omega, array.x, array.n, npw) + plot(d, selection, secondary_source) + + """ + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + n = util.normalize_vector(n) + k = util.wavenumber(omega, c) + d = 2j * k * np.inner(n, n0) * np.exp(-1j * k * np.inner(n, x0)) + selection = util.source_selection_plane(n0, n) + return d, selection, secondary_source_point(omega, c) + + +plane_2d = _plane + + +def plane_25d(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, + omalias=None): + r"""Driving function for 2.5-dimensional WFS for a virtual plane wave. + + Parameters + ---------- + omega : float + Angular frequency of plane wave. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + n : (3,) array_like, optional + Normal vector (traveling direction) of plane wave. + xref : (3,) array_like, optional + Reference point for synthesized sound field. + c : float, optional + Speed of sound. + omalias: float, optional + Angular frequency where spatial aliasing becomes prominent. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + .. math:: + + D_\text{2.5D}(\x_0,\w) = \sqrt{\i\wc |\x_\text{ref}-\x_0|} + \scalarprod{\n}{\n_0} + \e{-\i\wc \scalarprod{\n}{\x_0}} + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.wfs.plane_25d( + omega, array.x, array.n, npw) + plot(d, selection, secondary_source) + + """ + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + n = util.normalize_vector(n) + xref = util.asarray_1d(xref) + k = util.wavenumber(omega, c) + d = ( + preeq_25d(omega, omalias, c) * + np.sqrt(8*np.pi * np.linalg.norm(xref - x0, axis=-1)) * + np.inner(n, n0) * np.exp(-1j * k * np.inner(n, x0))) + selection = util.source_selection_plane(n0, n) + return d, selection, secondary_source_point(omega, c) + + +plane_3d = _plane + + +def _focused(omega, x0, n0, xs, ns, c=None): + r"""Driving function for 2/3-dimensional WFS for a focused source. + + Parameters + ---------- + omega : float + Angular frequency of focused source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs : (3,) array_like + Position of focused source. + ns : (3,) array_like + Direction of focused source. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + .. math:: + + D(\x_0,\w) = \i\wc \frac{\scalarprod{\x_0-\x_\text{s}}{\n_0}} + {|\x_0-\x_\text{s}|^\frac{3}{2}} + \e{\i\wc |\x_0-\x_\text{s}|} + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.wfs.focused_3d( + omega, array.x, array.n, xs_focused, ns_focused) + plot(d, selection, secondary_source) + + """ + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + xs = util.asarray_1d(xs) + k = util.wavenumber(omega, c) + ds = x0 - xs + r = np.linalg.norm(ds, axis=1) + d = 1j * k * inner1d(ds, n0) / r ** (3 / 2) * np.exp(1j * k * r) + selection = util.source_selection_focused(ns, x0, xs) + return d, selection, secondary_source_point(omega, c) + + +focused_2d = _focused + + +def focused_25d(omega, x0, n0, xs, ns, xref=[0, 0, 0], c=None, + omalias=None): + r"""Driving function for 2.5-dimensional WFS for a focused source. + + Parameters + ---------- + omega : float + Angular frequency of focused source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs : (3,) array_like + Position of focused source. + ns : (3,) array_like + Direction of focused source. + xref : (3,) array_like, optional + Reference point for synthesized sound field. + c : float, optional + Speed of sound. + omalias: float, optional + Angular frequency where spatial aliasing becomes prominent. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + .. math:: + + D(\x_0,\w) = \sqrt{\i\wc |\x_\text{ref}-\x_0|} + \frac{\scalarprod{\x_0-\x_\text{s}}{\n_0}} + {|\x_0-\x_\text{s}|^\frac{3}{2}} + \e{\i\wc |\x_0-\x_\text{s}|} + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.wfs.focused_25d( + omega, array.x, array.n, xs_focused, ns_focused) + plot(d, selection, secondary_source) + + """ + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + xs = util.asarray_1d(xs) + xref = util.asarray_1d(xref) + k = util.wavenumber(omega, c) + ds = x0 - xs + r = np.linalg.norm(ds, axis=1) + d = ( + preeq_25d(omega, omalias, c) * + np.sqrt(np.linalg.norm(xref - x0)) * inner1d(ds, n0) / + r ** (3 / 2) * np.exp(1j * k * r)) + selection = util.source_selection_focused(ns, x0, xs) + return d, selection, secondary_source_point(omega, c) + + +focused_3d = _focused + + +def preeq_25d(omega, omalias, c): + r"""Pre-equalization filter for 2.5-dimensional WFS. + + Parameters + ---------- + omega : float + Angular frequency. + omalias: float + Angular frequency where spatial aliasing becomes prominent. + c : float + Speed of sound. + + Returns + ------- + float + Complex weight for given angular frequency. + + Notes + ----- + .. math:: + + H(\w) = \begin{cases} + \sqrt{\i \wc} & \text{for } \w \leq \w_\text{alias} \\ + \sqrt{\i \frac{\w_\text{alias}}{c}} & \text{for } \w > \w_\text{alias} + \end{cases} + + """ + if omalias is None: + return np.sqrt(1j * util.wavenumber(omega, c)) + else: + if omega <= omalias: + return np.sqrt(1j * util.wavenumber(omega, c)) + else: + return np.sqrt(1j * util.wavenumber(omalias, c)) + + +def plane_3d_delay(omega, x0, n0, n=[0, 1, 0], c=None): + r"""Delay-only driving function for a virtual plane wave. + + Parameters + ---------- + omega : float + Angular frequency of plane wave. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + n : (3,) array_like, optional + Normal vector (traveling direction) of plane wave. + c : float, optional + Speed of sound. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + .. math:: + + D(\x_0,\w) = \e{-\i\wc\scalarprod{\n}{\x_0}} + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.wfs.plane_3d_delay( + omega, array.x, array.n, npw) + plot(d, selection, secondary_source) + + """ + x0 = util.asarray_of_rows(x0) + n = util.normalize_vector(n) + k = util.wavenumber(omega, c) + d = np.exp(-1j * k * np.inner(n, x0)) + selection = util.source_selection_plane(n0, n) + return d, selection, secondary_source_point(omega, c) + + +def soundfigure_3d(omega, x0, n0, figure, npw=[0, 0, 1], c=None): + """Compute driving function for a 2D sound figure. + + Based on + [Helwani et al., The Synthesis of Sound Figures, MSSP, 2013] + + """ + x0 = np.asarray(x0) + n0 = np.asarray(n0) + k = util.wavenumber(omega, c) + nx, ny = figure.shape + + # 2D spatial DFT of image + figure = np.fft.fftshift(figure, axes=(0, 1)) # sign of spatial DFT + figure = np.fft.fft2(figure) + # wavenumbers + kx = np.fft.fftfreq(nx, 1./nx) + ky = np.fft.fftfreq(ny, 1./ny) + # shift spectrum due to desired plane wave + figure = np.roll(figure, int(k*npw[0]), axis=0) + figure = np.roll(figure, int(k*npw[1]), axis=1) + # search and iterate over propagating plane wave components + kxx, kyy = np.meshgrid(kx, ky, sparse=True) + rho = np.sqrt((kxx) ** 2 + (kyy) ** 2) + d = 0 + for n in range(nx): + for m in range(ny): + if(rho[n, m] < k): + # dispertion relation + kz = np.sqrt(k**2 - rho[n, m]**2) + # normal vector of plane wave + npw = 1/k * np.asarray([kx[n], ky[m], kz]) + npw = npw / np.linalg.norm(npw) + # driving function of plane wave with positive kz + d_component, selection, secondary_source = plane_3d( + omega, x0, n0, npw, c) + d += selection * figure[n, m] * d_component + + return d, util.source_selection_all(len(d)), secondary_source From dcf111d321903bf6b3435d59d21d1f07fda24b0d Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 12 Mar 2019 09:17:47 +0100 Subject: [PATCH 052/101] DOC: README update --- README.rst | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index 6244ef9..0c3962e 100644 --- a/README.rst +++ b/README.rst @@ -1,29 +1,25 @@ -Sound Field Synthesis Toolbox for Python -======================================== +Sound Field Synthesis (SFS) Toolbox for Python +============================================== -The Sound Field Synthesis Toolbox for Python gives you the possibility to create -numercial simulations of sound field synthesis methods like wave field synthesis -(WFS) or near-field compensated higher order Ambisonics (NFC-HOA). - -Theory: - http://sfstoolbox.org/ +A Python library for creating numercial simulations of sound field synthesis +methods like Wave Field Synthesis (WFS) or Near-Field Compensated Higher Order +Ambisonics (NFC-HOA). Documentation: - http://python.sfstoolbox.org/ + https://sfs-python.readthedocs.io/ Source code and issue tracker: https://github.com/sfstoolbox/sfs-python/ -Python Package Index: - https://pypi.python.org/pypi/sfs/ - -SFS Toolbox for Matlab: - http://matlab.sfstoolbox.org/ - License: MIT -- see the file ``LICENSE`` for details. Quick start: - * Install NumPy, SciPy and Matplotlib + * Install Python 3, NumPy, SciPy and Matplotlib * ``python3 -m pip install sfs --user`` - * ``python3 doc/examples/horizontal_plane_arrays.py`` + * Check out the examples in the documentation + +More information about the underlying theory can be found at +https://sfs.readthedocs.io/. +There is also a Sound Field Synthesis Toolbox for Octave/Matlab, see +https://sfs-matlab.readthedocs.io/. From 4c0ba80153ac2b624875c9455fd9976722e7eec8 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 12 Mar 2019 09:35:34 +0100 Subject: [PATCH 053/101] DOC: Add markup to NEWS This generates links in the Sphinx docs --- NEWS.rst | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index 5994eac..94ca333 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -5,18 +5,19 @@ Version 0.4.0 (2018-03-14): * Driving functions in time domain for a plane wave, point source, and focused source * Image source model for a point source in a rectangular room - * DelayedSignal class and as_delayed_signal() + * `sfs.util.DelayedSignal` class and `sfs.util.as_delayed_signal()` * Improvements to the documentation * Start using Jupyter notebooks for examples in documentation - * Spherical Hankel function as util.spherical_hn2 - * Use spherical_jn, spherical_yn from scipy.special instead of sph_jnyn - * Generalization of the modal order argument in mono.source.point_modal() - * Rename util.normal_vector() to util.normalize_vector() - * Add parameter max_order to NFCHOA driving functions - * Add beta parameter to Kaiser tapering window + * Spherical Hankel function as `sfs.util.spherical_hn2()` + * Use `scipy.special.spherical_jn`, `scipy.special.spherical_yn` instead of + `scipy.special.sph_jnyn` + * Generalization of the modal order argument in `sfs.mono.source.point_modal()` + * Rename `sfs.util.normal_vector()` to `sfs.util.normalize_vector()` + * Add parameter ``max_order`` to NFCHOA driving functions + * Add ``beta`` parameter to Kaiser tapering window * Fix clipping problem of sound field plots with matplotlib 2.1 - * Fix elevation in util.cart2sph - * Fix tapering.tukey() for alpha=1 + * Fix elevation in `sfs.util.cart2sph()` + * Fix `sfs.tapering.tukey()` for ``alpha=1`` Version 0.3.1 (2016-04-08): * Fixed metadata of release @@ -26,12 +27,10 @@ Version 0.3.0 (2016-04-08): * Driving functions for the synthesis of various virtual source types with edge-shaped arrays by the equivalent scattering appoach * Driving functions for the synthesis of focused sources by WFS - * Several refactorings, bugfixes and other improvements Version 0.2.0 (2015-12-11): * Ability to calculate and plot particle velocity and displacement fields * Several function name and parameter name changes - * Several refactorings, bugfixes and other improvements Version 0.1.1 (2015-10-08): * Fix missing `sfs.mono` subpackage in PyPI packages From 6b5ed6a8edbbe266b89d5eafab37eef3e99d32b2 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 12 Mar 2019 10:00:45 +0100 Subject: [PATCH 054/101] DOC: Change some links to readthedocs.io --- doc/README | 5 +++-- doc/_template/layout.html | 6 +++--- sfs/__init__.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/README b/doc/README index deb2a66..2752b55 100644 --- a/doc/README +++ b/doc/README @@ -1,7 +1,8 @@ This directory holds the documentation in reStructuredText/Sphinx format. +It also contains some examples (Jupyter notebooks and Python scripts). Have a look at the online documentation for the auto-generated HTML version: -http://python.sfstoolbox.org/ +https://sfs-python.readthedocs.io/ If you want to generate the HTML (or LaTeX/PDF) files on your computer, have a -look at http://python.sfstoolbox.org/en/latest/contributing.html. +look at https://sfs-python.readthedocs.io/en/latest/contributing.html. diff --git a/doc/_template/layout.html b/doc/_template/layout.html index 478e1fd..36a3c25 100644 --- a/doc/_template/layout.html +++ b/doc/_template/layout.html @@ -6,9 +6,9 @@ {% include "searchbox.html" %} + {% endblock %} diff --git a/sfs/__init__.py b/sfs/__init__.py index eaae8e9..8271b28 100644 --- a/sfs/__init__.py +++ b/sfs/__init__.py @@ -1,6 +1,6 @@ """Sound Field Synthesis Toolbox. -http://python.sfstoolbox.org/ +https://sfs-python.readthedocs.io/ .. rubric:: Submodules From 966e43c26e13c3c7df80d4c3b2874721f0b3d79a Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 12 Mar 2019 09:54:11 +0100 Subject: [PATCH 055/101] DOC: Update CONTRIBUTING --- CONTRIBUTING.rst | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 19af082..480773c 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -9,23 +9,21 @@ Contributions are always welcome! Development Installation ^^^^^^^^^^^^^^^^^^^^^^^^ -Instead of pip-installing the latest release from PyPI, you should get the +Instead of pip-installing the latest release from PyPI_, you should get the newest development version from Github_:: git clone https://github.com/sfstoolbox/sfs-python.git cd sfs-python - python3 setup.py develop --user + python3 -m pip install --user -e . -.. _Github: https://github.com/sfstoolbox/sfs-python/ +... where ``-e`` stands for ``--editable``. This way, your installation always stays up-to-date, even if you pull new changes from the Github repository. -If you prefer, you can also replace the last command with:: - - python3 -m pip install --user -e . +.. _PyPI: https://pypi.org/project/sfs/ +.. _Github: https://github.com/sfstoolbox/sfs-python/ -... where ``-e`` stands for ``--editable``. Building the Documentation ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -75,12 +73,12 @@ New releases are made using the following steps: #. Create a source distribution with ``python3 setup.py sdist`` #. Create a wheel distribution with ``python3 setup.py bdist_wheel`` #. Check that both files have the correct content -#. Upload them to PyPI with twine_: ``twine upload dist/*`` +#. Upload them to PyPI_ with twine_: ``python3 -m twine upload dist/*`` #. Push the commit and the tag to Github and `add release notes`_ containing a link to PyPI and the bullet points from ``NEWS.rst`` -#. Check that the new release was built correctly on RTD_, delete the "stable" - version and select the new release as default version +#. Check that the new release was built correctly on RTD_ + and select the new release as default version -.. _twine: https://pypi.org/project/twine/ +.. _twine: https://twine.readthedocs.io/ .. _add release notes: https://github.com/sfstoolbox/sfs-python/tags -.. _RTD: http://readthedocs.org/projects/sfs-python/builds/ +.. _RTD: https://readthedocs.org/projects/sfs-python/builds/ From a30f08fe9b5f82eb992464b27d4997b722bff652 Mon Sep 17 00:00:00 2001 From: Frank Schultz Date: Wed, 13 Mar 2019 15:19:17 +0100 Subject: [PATCH 056/101] New time driving function handling (#126) --- doc/examples/time_domain.py | 12 +- doc/examples/time_domain_nfchoa.py | 8 +- sfs/time/__init__.py | 55 +++- sfs/time/{drivingfunction.py => nfchoa.py} | 343 +-------------------- sfs/time/wfs.py | 318 +++++++++++++++++++ 5 files changed, 391 insertions(+), 345 deletions(-) rename sfs/time/{drivingfunction.py => nfchoa.py} (58%) create mode 100644 sfs/time/wfs.py diff --git a/doc/examples/time_domain.py b/doc/examples/time_domain.py index 953769f..8f64d9a 100644 --- a/doc/examples/time_domain.py +++ b/doc/examples/time_domain.py @@ -25,8 +25,8 @@ t = 0.008 # compute driving signals d_delay, d_weight, selection, secondary_source = \ - sfs.time.drivingfunction.wfs_25d_point(array.x, array.n, xs) -d = sfs.time.drivingfunction.driving_signals(d_delay, d_weight, signal) + sfs.time.wfs.point_25d(array.x, array.n, xs) +d = sfs.time.wfs.driving_signals(d_delay, d_weight, signal) # test soundfield twin = sfs.tapering.tukey(selection, .3) @@ -50,8 +50,8 @@ # compute driving signals d_delay, d_weight, selection, secondary_source = \ - sfs.time.drivingfunction.wfs_25d_plane(array.x, array.n, npw) -d = sfs.time.drivingfunction.driving_signals(d_delay, d_weight, signal) + sfs.time.wfs.plane_25d(array.x, array.n, npw) +d = sfs.time.wfs.driving_signals(d_delay, d_weight, signal) # test soundfield twin = sfs.tapering.tukey(selection, .3) @@ -72,8 +72,8 @@ nfs = sfs.util.normalize_vector(xref - xs) # main n of fsource t = 0.003 # compute driving signals d_delay, d_weight, selection, secondary_source = \ - sfs.time.drivingfunction.wfs_25d_focused(array.x, array.n, xs, nfs) -d = sfs.time.drivingfunction.driving_signals(d_delay, d_weight, signal) + sfs.time.wfs.focused_25d(array.x, array.n, xs, nfs) +d = sfs.time.wfs.driving_signals(d_delay, d_weight, signal) # test soundfield twin = sfs.tapering.tukey(selection, .3) diff --git a/doc/examples/time_domain_nfchoa.py b/doc/examples/time_domain_nfchoa.py index 6195b99..204f95e 100644 --- a/doc/examples/time_domain_nfchoa.py +++ b/doc/examples/time_domain_nfchoa.py @@ -20,8 +20,8 @@ npw = [0, -1, 0] # propagating direction t = 0 # observation time delay, weight, sos, phaseshift, selection, secondary_source = \ - sfs.time.drivingfunction.nfchoa_25d_plane(array.x, R, npw, fs, max_order) -d = sfs.time.drivingfunction.nfchoa_25d_driving_signals( + sfs.time.nfchoa.plane_25d(array.x, R, npw, fs, max_order) +d = sfs.time.nfchoa.driving_signals_25d( delay, weight, sos, phaseshift, signal) p = sfs.time.synthesize(d, selection, array, secondary_source, observation_time=t, grid=grid) @@ -37,8 +37,8 @@ xs = [1.5, 1.5, 0] # position t = np.linalg.norm(xs) / sfs.default.c # observation time delay, weight, sos, phaseshift, selection, secondary_source = \ - sfs.time.drivingfunction.nfchoa_25d_point(array.x, R, xs, fs, max_order) -d = sfs.time.drivingfunction.nfchoa_25d_driving_signals( + sfs.time.nfchoa.point_25d(array.x, R, xs, fs, max_order) +d = sfs.time.nfchoa.driving_signals_25d( delay, weight, sos, phaseshift, signal) p = sfs.time.synthesize(d, selection, array, secondary_source, observation_time=t, grid=grid) diff --git a/sfs/time/__init__.py b/sfs/time/__init__.py index 48cc2cc..15ce5b4 100644 --- a/sfs/time/__init__.py +++ b/sfs/time/__init__.py @@ -3,15 +3,16 @@ .. autosummary:: :toctree: - drivingfunction source + wfs + nfchoa + """ -from . import drivingfunction +from .. import array as _array +import numpy as np from . import source - from .. import util as _util -from .. import array as _array def synthesize(signals, weights, ssd, secondary_source_function, **kwargs): @@ -63,3 +64,49 @@ def synthesize(signals, weights, ssd, secondary_source_function, **kwargs): signal = channel, samplerate, signal_offset p += a * weight * secondary_source_function(x, n, signal, **kwargs) return p + + +def apply_delays(signal, delays): + """Apply delays for every channel. + + Parameters + ---------- + signal : (N,) array_like + float + Excitation signal consisting of (mono) audio data and a sampling + rate (in Hertz). A `DelayedSignal` object can also be used. + delays : (C,) array_like + Delay in seconds for each channel (C), negative values allowed. + + Returns + ------- + `DelayedSignal` + A tuple containing the delayed signals (in a `numpy.ndarray` + with shape ``(N, C)``), followed by the sampling rate (in Hertz) + and a (possibly negative) time offset (in seconds). + + """ + data, samplerate, initial_offset = _util.as_delayed_signal(signal) + data = _util.asarray_1d(data) + delays = _util.asarray_1d(delays) + delays += initial_offset + + delays_samples = np.rint(samplerate * delays).astype(int) + offset_samples = delays_samples.min() + delays_samples -= offset_samples + out = np.zeros((delays_samples.max() + len(data), len(delays_samples))) + for column, row in enumerate(delays_samples): + out[row:row + len(data), column] = data + return _util.DelayedSignal(out, samplerate, offset_samples / samplerate) + + +def secondary_source_point(c): + """Create a point source for use in `sfs.time.synthesize()`.""" + + def secondary_source(position, _, signal, observation_time, grid): + return source.point(position, signal, observation_time, grid, c=c) + + return secondary_source + + +from . import nfchoa +from . import wfs diff --git a/sfs/time/drivingfunction.py b/sfs/time/nfchoa.py similarity index 58% rename from sfs/time/drivingfunction.py rename to sfs/time/nfchoa.py index 2c942d5..8c6f77f 100644 --- a/sfs/time/drivingfunction.py +++ b/sfs/time/nfchoa.py @@ -1,4 +1,4 @@ -"""Compute time based driving functions for various systems. +"""Compute NFC-HOA driving functions. .. include:: math-definitions.rst @@ -18,12 +18,6 @@ rs = np.linalg.norm(xs) # distance from origin ts = rs / sfs.default.c # time-of-arrival at origin - # Focused source - xf = -0.5, 0.5, 0 - nf = sfs.util.direction_vector(np.radians(-45)) # normal vector - rf = np.linalg.norm(xf) # distance from origin - tf = rf / sfs.default.c # time-of-arrival at origin - # Impulsive excitation fs = 44100 signal = unit_impulse(512), fs @@ -43,323 +37,11 @@ def plot(d, selection, secondary_source, t=0): """ import numpy as np -from numpy.core.umath_tests import inner1d # element-wise inner product from scipy.signal import besselap, sosfilt, zpk2sos from scipy.special import eval_legendre as legendre from .. import default from .. import util -from . import source as _source - - -def wfs_25d_plane(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): - r"""Plane wave model by 2.5-dimensional WFS. - - Parameters - ---------- - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of secondary source orientations. - n : (3,) array_like, optional - Normal vector (propagation direction) of synthesized plane wave. - xref : (3,) array_like, optional - Reference position - c : float, optional - Speed of sound - - Returns - ------- - delays : (N,) numpy.ndarray - Delays of secondary sources in seconds. - weights : (N,) numpy.ndarray - Weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.time.synthesize()`. - - Notes - ----- - 2.5D correction factor - - .. math:: - - g_0 = \sqrt{2 \pi |x_\mathrm{ref} - x_0|} - - d using a plane wave as source model - - .. math:: - - d_{2.5D}(x_0,t) = h(t) - 2 g_0 \scalarprod{n}{n_0} - \dirac{t - \frac{1}{c} \scalarprod{n}{x_0}} - - with wfs(2.5D) prefilter h(t), which is not implemented yet. - - References - ---------- - See http://sfstoolbox.org/en/latest/#equation-d.wfs.pw.2.5D - - Examples - -------- - .. plot:: - :context: close-figs - - delays, weights, selection, secondary_source = \ - sfs.time.drivingfunction.wfs_25d_plane(array.x, array.n, npw) - d = sfs.time.drivingfunction.driving_signals(delays, weights, signal) - plot(d, selection, secondary_source) - - """ - if c is None: - c = default.c - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - n = util.normalize_vector(n) - xref = util.asarray_1d(xref) - g0 = np.sqrt(2 * np.pi * np.linalg.norm(xref - x0, axis=1)) - delays = inner1d(n, x0) / c - weights = 2 * g0 * inner1d(n, n0) - selection = util.source_selection_plane(n0, n) - return delays, weights, selection, secondary_source_point(c) - - -def wfs_25d_point(x0, n0, xs, xref=[0, 0, 0], c=None): - r"""Point source by 2.5-dimensional WFS. - - Parameters - ---------- - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of secondary source orientations. - xs : (3,) array_like - Virtual source position. - xref : (3,) array_like, optional - Reference position - c : float, optional - Speed of sound - - Returns - ------- - delays : (N,) numpy.ndarray - Delays of secondary sources in seconds. - weights: (N,) numpy.ndarray - Weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.time.synthesize()`. - - Notes - ----- - 2.5D correction factor - - .. math:: - - g_0 = \sqrt{2 \pi |x_\mathrm{ref} - x_0|} - - - d using a point source as source model - - .. math:: - - d_{2.5D}(x_0,t) = h(t) - \frac{g_0 \scalarprod{(x_0 - x_s)}{n_0}} - {2\pi |x_0 - x_s|^{3/2}} - \dirac{t - \frac{|x_0 - x_s|}{c}} - - with wfs(2.5D) prefilter h(t), which is not implemented yet. - - References - ---------- - See http://sfstoolbox.org/en/latest/#equation-d.wfs.ps.2.5D - - Examples - -------- - .. plot:: - :context: close-figs - - delays, weights, selection, secondary_source = \ - sfs.time.drivingfunction.wfs_25d_point(array.x, array.n, xs) - d = sfs.time.drivingfunction.driving_signals(delays, weights, signal) - plot(d, selection, secondary_source, t=ts) - - """ - if c is None: - c = default.c - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - xref = util.asarray_1d(xref) - g0 = np.sqrt(2 * np.pi * np.linalg.norm(xref - x0, axis=1)) - ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - delays = r/c - weights = g0 * inner1d(ds, n0) / (2 * np.pi * r**(3/2)) - selection = util.source_selection_point(n0, x0, xs) - return delays, weights, selection, secondary_source_point(c) - - -def wfs_25d_focused(x0, n0, xs, ns, xref=[0, 0, 0], c=None): - r"""Point source by 2.5-dimensional WFS. - - Parameters - ---------- - x0 : (N, 3) array_like - Sequence of secondary source positions. - n0 : (N, 3) array_like - Sequence of secondary source orientations. - xs : (3,) array_like - Virtual source position. - ns : (3,) array_like - Normal vector (propagation direction) of focused source. - This is used for secondary source selection, - see `sfs.util.source_selection_focused()`. - xref : (3,) array_like, optional - Reference position - c : float, optional - Speed of sound - - Returns - ------- - delays : (N,) numpy.ndarray - Delays of secondary sources in seconds. - weights: (N,) numpy.ndarray - Weights of secondary sources. - selection : (N,) numpy.ndarray - Boolean array containing ``True`` or ``False`` depending on - whether the corresponding secondary source is "active" or not. - secondary_source_function : callable - A function that can be used to create the sound field of a - single secondary source. See `sfs.time.synthesize()`. - - Notes - ----- - 2.5D correction factor - - .. math:: - - g_0 = \sqrt{\frac{|x_\mathrm{ref} - x_0|} - {|x_0-x_s| + |x_\mathrm{ref}-x_0|}} - - - d using a point source as source model - - .. math:: - - d_{2.5D}(x_0,t) = h(t) - \frac{g_0 \scalarprod{(x_0 - x_s)}{n_0}} - {|x_0 - x_s|^{3/2}} - \dirac{t + \frac{|x_0 - x_s|}{c}} - - with wfs(2.5D) prefilter h(t), which is not implemented yet. - - References - ---------- - See http://sfstoolbox.org/en/latest/#equation-d.wfs.fs.2.5D - - Examples - -------- - .. plot:: - :context: close-figs - - delays, weights, selection, secondary_source = \ - sfs.time.drivingfunction.wfs_25d_focused(array.x, array.n, xf, nf) - d = sfs.time.drivingfunction.driving_signals(delays, weights, signal) - plot(d, selection, secondary_source, t=tf) - - """ - if c is None: - c = default.c - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - xref = util.asarray_1d(xref) - ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - g0 = np.sqrt(np.linalg.norm(xref - x0, axis=1) - / (np.linalg.norm(xref - x0, axis=1) + r)) - delays = -r/c - weights = g0 * inner1d(ds, n0) / (2 * np.pi * r**(3/2)) - selection = util.source_selection_focused(ns, x0, xs) - return delays, weights, selection, secondary_source_point(c) - - -def driving_signals(delays, weights, signal): - """Get driving signals per secondary source. - - Returned signals are the delayed and weighted mono input signal - (with N samples) per channel (C). - - Parameters - ---------- - delays : (C,) array_like - Delay in seconds for each channel, negative values allowed. - weights : (C,) array_like - Amplitude weighting factor for each channel. - signal : (N,) array_like + float - Excitation signal consisting of (mono) audio data and a sampling - rate (in Hertz). A `DelayedSignal` object can also be used. - - Returns - ------- - `DelayedSignal` - A tuple containing the driving signals (in a `numpy.ndarray` - with shape ``(N, C)``), followed by the sampling rate (in Hertz) - and a (possibly negative) time offset (in seconds). - - """ - delays = util.asarray_1d(delays) - weights = util.asarray_1d(weights) - data, samplerate, signal_offset = apply_delays(signal, delays) - return util.DelayedSignal(data * weights, samplerate, signal_offset) - - -def apply_delays(signal, delays): - """Apply delays for every channel. - - Parameters - ---------- - signal : (N,) array_like + float - Excitation signal consisting of (mono) audio data and a sampling - rate (in Hertz). A `DelayedSignal` object can also be used. - delays : (C,) array_like - Delay in seconds for each channel (C), negative values allowed. - - Returns - ------- - `DelayedSignal` - A tuple containing the delayed signals (in a `numpy.ndarray` - with shape ``(N, C)``), followed by the sampling rate (in Hertz) - and a (possibly negative) time offset (in seconds). - - """ - data, samplerate, initial_offset = util.as_delayed_signal(signal) - data = util.asarray_1d(data) - delays = util.asarray_1d(delays) - delays += initial_offset - - delays_samples = np.rint(samplerate * delays).astype(int) - offset_samples = delays_samples.min() - delays_samples -= offset_samples - out = np.zeros((delays_samples.max() + len(data), len(delays_samples))) - for column, row in enumerate(delays_samples): - out[row:row + len(data), column] = data - return util.DelayedSignal(out, samplerate, offset_samples / samplerate) - - -def secondary_source_point(c): - """Create a point source for use in `sfs.time.synthesize()`.""" - - def secondary_source(position, _, signal, observation_time, grid): - return _source.point(position, signal, observation_time, grid, c=c) - - return secondary_source +from . import secondary_source_point def matchedz_zpk(s_zeros, s_poles, s_gain, fs): @@ -398,8 +80,7 @@ def matchedz_zpk(s_zeros, s_poles, s_gain, fs): return z_zeros, z_poles, np.real(s_gain) -def nfchoa_25d_plane(x0, r0, npw, fs, max_order=None, c=None, - s2z=matchedz_zpk): +def plane_25d(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk): r"""Virtual plane wave by 2.5-dimensional NFC-HOA. .. math:: @@ -457,8 +138,8 @@ def nfchoa_25d_plane(x0, r0, npw, fs, max_order=None, c=None, :context: close-figs delay, weight, sos, phaseshift, selection, secondary_source = \ - sfs.time.drivingfunction.nfchoa_25d_plane(array.x, R, npw, fs) - d = sfs.time.drivingfunction.nfchoa_25d_driving_signals( + sfs.time.nfchoa.plane_25d(array.x, R, npw, fs) + d = sfs.time.nfchoa.driving_signals_25d( delay, weight, sos, phaseshift, signal) plot(d, selection, secondary_source) @@ -488,7 +169,7 @@ def nfchoa_25d_plane(x0, r0, npw, fs, max_order=None, c=None, return delay, weight, sos, phaseshift, selection, secondary_source_point(c) -def nfchoa_25d_point(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): +def point_25d(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): r"""Virtual Point source by 2.5-dimensional NFC-HOA. .. math:: @@ -547,8 +228,8 @@ def nfchoa_25d_point(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): :context: close-figs delay, weight, sos, phaseshift, selection, secondary_source = \ - sfs.time.drivingfunction.nfchoa_25d_point(array.x, R, xs, fs) - d = sfs.time.drivingfunction.nfchoa_25d_driving_signals( + sfs.time.nfchoa.point_25d(array.x, R, xs, fs) + d = sfs.time.nfchoa.driving_signals_25d( delay, weight, sos, phaseshift, signal) plot(d, selection, secondary_source, t=ts) @@ -578,7 +259,7 @@ def nfchoa_25d_point(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): return delay, weight, sos, phaseshift, selection, secondary_source_point(c) -def nfchoa_3d_plane(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk): +def plane_3d(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk): r"""Virtual plane wave by 3-dimensional NFC-HOA. .. math:: @@ -658,7 +339,7 @@ def nfchoa_3d_plane(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk): return delay, weight, sos, phaseshift, selection, secondary_source_point(c) -def nfchoa_3d_point(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): +def point_3d(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): r"""Virtual point source by 3-dimensional NFC-HOA. .. math:: @@ -739,7 +420,7 @@ def nfchoa_3d_point(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): return delay, weight, sos, phaseshift, selection, secondary_source_point(c) -def nfchoa_25d_driving_signals(delay, weight, sos, phaseshift, signal): +def driving_signals_25d(delay, weight, sos, phaseshift, signal): """Get 2.5-dimensional NFC-HOA driving signals. Parameters @@ -773,7 +454,7 @@ def nfchoa_25d_driving_signals(delay, weight, sos, phaseshift, signal): return util.DelayedSignal(2 * weight * out, fs, t_offset + delay) -def nfchoa_3d_driving_signals(delay, weight, sos, phaseshift, signal): +def driving_signals_3d(delay, weight, sos, phaseshift, signal): """Get 3-dimensional NFC-HOA driving signals. Parameters diff --git a/sfs/time/wfs.py b/sfs/time/wfs.py new file mode 100644 index 0000000..a9280a0 --- /dev/null +++ b/sfs/time/wfs.py @@ -0,0 +1,318 @@ +"""Compute WFS driving functions. + +.. include:: math-definitions.rst + +.. plot:: + :context: reset + + import matplotlib.pyplot as plt + import numpy as np + import sfs + from scipy.signal import unit_impulse + + # Plane wave + npw = sfs.util.direction_vector(np.radians(-45)) + + # Point source + xs = -1.5, 1.5, 0 + rs = np.linalg.norm(xs) # distance from origin + ts = rs / sfs.default.c # time-of-arrival at origin + + # Focused source + xf = -0.5, 0.5, 0 + nf = sfs.util.direction_vector(np.radians(-45)) # normal vector + rf = np.linalg.norm(xf) # distance from origin + tf = rf / sfs.default.c # time-of-arrival at origin + + # Impulsive excitation + fs = 44100 + signal = unit_impulse(512), fs + + # Circular loudspeaker array + N = 32 # number of loudspeakers + R = 1.5 # radius + array = sfs.array.circular(N, R) + + grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02) + + def plot(d, selection, secondary_source, t=0): + p = sfs.time.synthesize(d, selection, array, secondary_source, grid=grid, + observation_time=t) + sfs.plot.level(p, grid) + sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) + +""" +import numpy as np +from numpy.core.umath_tests import inner1d # element-wise inner product +from .. import default +from .. import util +from . import secondary_source_point, apply_delays + + +def plane_25d(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): + r"""Plane wave model by 2.5-dimensional WFS. + + Parameters + ---------- + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of secondary source orientations. + n : (3,) array_like, optional + Normal vector (propagation direction) of synthesized plane wave. + xref : (3,) array_like, optional + Reference position + c : float, optional + Speed of sound + + Returns + ------- + delays : (N,) numpy.ndarray + Delays of secondary sources in seconds. + weights : (N,) numpy.ndarray + Weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.time.synthesize()`. + + Notes + ----- + 2.5D correction factor + + .. math:: + + g_0 = \sqrt{2 \pi |x_\mathrm{ref} - x_0|} + + d using a plane wave as source model + + .. math:: + + d_{2.5D}(x_0,t) = h(t) + 2 g_0 \scalarprod{n}{n_0} + \dirac{t - \frac{1}{c} \scalarprod{n}{x_0}} + + with wfs(2.5D) prefilter h(t), which is not implemented yet. + + References + ---------- + See http://sfstoolbox.org/en/latest/#equation-d.wfs.pw.2.5D + + Examples + -------- + .. plot:: + :context: close-figs + + delays, weights, selection, secondary_source = \ + sfs.time.wfs.plane_25d(array.x, array.n, npw) + d = sfs.time.wfs.driving_signals(delays, weights, signal) + plot(d, selection, secondary_source) + + """ + if c is None: + c = default.c + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + n = util.normalize_vector(n) + xref = util.asarray_1d(xref) + g0 = np.sqrt(2 * np.pi * np.linalg.norm(xref - x0, axis=1)) + delays = inner1d(n, x0) / c + weights = 2 * g0 * inner1d(n, n0) + selection = util.source_selection_plane(n0, n) + return delays, weights, selection, secondary_source_point(c) + + +def point_25d(x0, n0, xs, xref=[0, 0, 0], c=None): + r"""Point source by 2.5-dimensional WFS. + + Parameters + ---------- + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of secondary source orientations. + xs : (3,) array_like + Virtual source position. + xref : (3,) array_like, optional + Reference position + c : float, optional + Speed of sound + + Returns + ------- + delays : (N,) numpy.ndarray + Delays of secondary sources in seconds. + weights: (N,) numpy.ndarray + Weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.time.synthesize()`. + + Notes + ----- + 2.5D correction factor + + .. math:: + + g_0 = \sqrt{2 \pi |x_\mathrm{ref} - x_0|} + + + d using a point source as source model + + .. math:: + + d_{2.5D}(x_0,t) = h(t) + \frac{g_0 \scalarprod{(x_0 - x_s)}{n_0}} + {2\pi |x_0 - x_s|^{3/2}} + \dirac{t - \frac{|x_0 - x_s|}{c}} + + with wfs(2.5D) prefilter h(t), which is not implemented yet. + + References + ---------- + See http://sfstoolbox.org/en/latest/#equation-d.wfs.ps.2.5D + + Examples + -------- + .. plot:: + :context: close-figs + + delays, weights, selection, secondary_source = \ + sfs.time.wfs.point_25d(array.x, array.n, xs) + d = sfs.time.wfs.driving_signals(delays, weights, signal) + plot(d, selection, secondary_source, t=ts) + + """ + if c is None: + c = default.c + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + xs = util.asarray_1d(xs) + xref = util.asarray_1d(xref) + g0 = np.sqrt(2 * np.pi * np.linalg.norm(xref - x0, axis=1)) + ds = x0 - xs + r = np.linalg.norm(ds, axis=1) + delays = r/c + weights = g0 * inner1d(ds, n0) / (2 * np.pi * r**(3/2)) + selection = util.source_selection_point(n0, x0, xs) + return delays, weights, selection, secondary_source_point(c) + + +def focused_25d(x0, n0, xs, ns, xref=[0, 0, 0], c=None): + r"""Point source by 2.5-dimensional WFS. + + Parameters + ---------- + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of secondary source orientations. + xs : (3,) array_like + Virtual source position. + ns : (3,) array_like + Normal vector (propagation direction) of focused source. + This is used for secondary source selection, + see `sfs.util.source_selection_focused()`. + xref : (3,) array_like, optional + Reference position + c : float, optional + Speed of sound + + Returns + ------- + delays : (N,) numpy.ndarray + Delays of secondary sources in seconds. + weights: (N,) numpy.ndarray + Weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.time.synthesize()`. + + Notes + ----- + 2.5D correction factor + + .. math:: + + g_0 = \sqrt{\frac{|x_\mathrm{ref} - x_0|} + {|x_0-x_s| + |x_\mathrm{ref}-x_0|}} + + + d using a point source as source model + + .. math:: + + d_{2.5D}(x_0,t) = h(t) + \frac{g_0 \scalarprod{(x_0 - x_s)}{n_0}} + {|x_0 - x_s|^{3/2}} + \dirac{t + \frac{|x_0 - x_s|}{c}} + + with wfs(2.5D) prefilter h(t), which is not implemented yet. + + References + ---------- + See http://sfstoolbox.org/en/latest/#equation-d.wfs.fs.2.5D + + Examples + -------- + .. plot:: + :context: close-figs + + delays, weights, selection, secondary_source = \ + sfs.time.wfs.focused_25d(array.x, array.n, xf, nf) + d = sfs.time.wfs.driving_signals(delays, weights, signal) + plot(d, selection, secondary_source, t=tf) + + """ + if c is None: + c = default.c + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + xs = util.asarray_1d(xs) + xref = util.asarray_1d(xref) + ds = x0 - xs + r = np.linalg.norm(ds, axis=1) + g0 = np.sqrt(np.linalg.norm(xref - x0, axis=1) + / (np.linalg.norm(xref - x0, axis=1) + r)) + delays = -r/c + weights = g0 * inner1d(ds, n0) / (2 * np.pi * r**(3/2)) + selection = util.source_selection_focused(ns, x0, xs) + return delays, weights, selection, secondary_source_point(c) + + +def driving_signals(delays, weights, signal): + """Get driving signals per secondary source. + + Returned signals are the delayed and weighted mono input signal + (with N samples) per channel (C). + + Parameters + ---------- + delays : (C,) array_like + Delay in seconds for each channel, negative values allowed. + weights : (C,) array_like + Amplitude weighting factor for each channel. + signal : (N,) array_like + float + Excitation signal consisting of (mono) audio data and a sampling + rate (in Hertz). A `DelayedSignal` object can also be used. + + Returns + ------- + `DelayedSignal` + A tuple containing the driving signals (in a `numpy.ndarray` + with shape ``(N, C)``), followed by the sampling rate (in Hertz) + and a (possibly negative) time offset (in seconds). + + """ + delays = util.asarray_1d(delays) + weights = util.asarray_1d(weights) + data, samplerate, signal_offset = apply_delays(signal, delays) + return util.DelayedSignal(data * weights, samplerate, signal_offset) From d8a5f0e70eb8d036ad9b7b066d718cd05151d9c8 Mon Sep 17 00:00:00 2001 From: Nara Hahn Date: Wed, 13 Mar 2019 16:27:54 +0100 Subject: [PATCH 057/101] Change to_html5_video() to to_jshtml() --- .../animations-pulsating-sphere.ipynb | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/doc/examples/animations-pulsating-sphere.ipynb b/doc/examples/animations-pulsating-sphere.ipynb index a0d7da2..fcf6408 100644 --- a/doc/examples/animations-pulsating-sphere.ipynb +++ b/doc/examples/animations-pulsating-sphere.ipynb @@ -92,22 +92,6 @@ "ani = animation.particle_displacement(\n", " omega, center, radius, amplitude, grid, frames, figsize, c='Gray')\n", "plt.close()\n", - "HTML(ani.to_html5_video())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can play with the animation more interactively by using `.to_jshtml`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ "HTML(ani.to_jshtml())" ] }, @@ -115,6 +99,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "Click the arrow button to start the animation.\n", + "`to_jshtml()` allows you to play with the animation,\n", + "e.g. speed up/down the animation (+/- button).\n", + "Try to reverse the playback by clicking the left arrow.\n", + "You'll see a sound _sink_.\n", + "\n", + "You can also show the animation by using `to_html5_video()`.\n", + "See the [documentation](https://matplotlib.org/api/_as_gen/matplotlib.animation.ArtistAnimation.html#matplotlib.animation.ArtistAnimation.to_html5_video) for more detail.\n", + "\n", "Of course, different types of grid can be chosen.\n", "Below is the particle animation using the same parameters\n", "but with a [hexagonal grid](https://www.redblobgames.com/grids/hexagons/)." @@ -158,7 +151,7 @@ "ani = animation.particle_displacement(\n", " omega, center, radius, amplitude, grid, frames, figsize, c='Gray')\n", "plt.close()\n", - "HTML(ani.to_html5_video())" + "HTML(ani.to_jshtml())" ] }, { @@ -179,7 +172,7 @@ "ani = animation.particle_displacement(\n", " omega, center, radius, amplitude, grid, frames, figsize, c='Gray')\n", "plt.close()\n", - "HTML(ani.to_html5_video())" + "HTML(ani.to_jshtml())" ] }, { @@ -208,7 +201,7 @@ "ani = animation.particle_velocity(\n", " omega, center, radius, amplitude, grid, frames, figsize)\n", "plt.close()\n", - "HTML(ani.to_html5_video())" + "HTML(ani.to_jshtml())" ] }, { @@ -254,7 +247,7 @@ " omega, center, radius, amplitude, grid, frames, pulsate=True,\n", " figsize=figsize, vmin=-max_pressure, vmax=max_pressure)\n", "plt.close()\n", - "HTML(ani.to_html5_video())" + "HTML(ani.to_jshtml())" ] }, { @@ -296,7 +289,7 @@ " omega, center, radius, amplitude, grid, frames, pulsate=True,\n", " figsize=figsize, vmin=-max_pressure, vmax=max_pressure)\n", "plt.close()\n", - "HTML(ani.to_html5_video())" + "HTML(ani.to_jshtml())" ] }, { @@ -328,7 +321,7 @@ " omega, center, radius, amplitude, grid, frames, pulsate=True,\n", " figsize=figsize, vmin=-max_pressure, vmax=max_pressure)\n", "plt.close()\n", - "HTML(ani.to_html5_video())" + "HTML(ani.to_jshtml())" ] }, { From da8b9ac460dc6fe1c78919b7eed052d94ca46f17 Mon Sep 17 00:00:00 2001 From: Nara Hahn Date: Mon, 11 Mar 2019 12:54:09 +0100 Subject: [PATCH 058/101] Add new returns in docstring --- sfs/time/nfchoa.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/sfs/time/nfchoa.py b/sfs/time/nfchoa.py index 8c6f77f..cbdf2ba 100644 --- a/sfs/time/nfchoa.py +++ b/sfs/time/nfchoa.py @@ -131,6 +131,12 @@ def plane_25d(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk): Second-order section filters :func:`scipy.signal.sosfilt`. phaseshift : (N,) numpy.ndarray Phase shift in radians. + selection : (N,) numpy.ndarray + Boolean array containing only ``True`` indicating that + all secondary source are "active" for NFC-HOA. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.time.synthesize()`. Examples -------- @@ -221,6 +227,12 @@ def point_25d(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): Second-order section filters :func:`scipy.signal.sosfilt`. phaseshift : (N,) numpy.ndarray Phase shift in radians. + selection : (N,) numpy.ndarray + Boolean array containing only ``True`` indicating that + all secondary source are "active" for NFC-HOA. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.time.synthesize()`. Examples -------- @@ -312,6 +324,12 @@ def plane_3d(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk): Second-order section filters :func:`scipy.signal.sosfilt`. phaseshift : (N,) numpy.ndarray Phase shift in radians. + selection : (N,) numpy.ndarray + Boolean array containing only ``True`` indicating that + all secondary source are "active" for NFC-HOA. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.time.synthesize()`. """ if max_order is None: @@ -393,6 +411,12 @@ def point_3d(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): Second-order section filters :func:`scipy.signal.sosfilt`. phaseshift : (N,) numpy.ndarray Phase shift in radians. + selection : (N,) numpy.ndarray + Boolean array containing only ``True`` indicating that + all secondary source are "active" for NFC-HOA. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.time.synthesize()`. """ if max_order is None: From 795d8190c036389bbf3fdc9271d647a158c26854 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 13 Mar 2019 14:58:13 +0100 Subject: [PATCH 059/101] DOC: use \mathtt{} for "max_order" --- sfs/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sfs/util.py b/sfs/util.py index dbfc307..d27311d 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -625,7 +625,7 @@ def max_order_circular_harmonics(N): It is given on page 132 of [Ahrens2012]_ as .. math:: - \text{max\_order} = + \mathtt{max\_order} = \begin{cases} N/2 - 1 & \text{even}\;N \\ (N-1)/2 & \text{odd}\;N, @@ -634,7 +634,7 @@ def max_order_circular_harmonics(N): which is equivalent to .. math:: - \text{max\_order} = \big\lfloor \frac{N - 1}{2} \big\rfloor. + \mathtt{max\_order} = \big\lfloor \frac{N - 1}{2} \big\rfloor. Parameters ---------- @@ -649,7 +649,7 @@ def max_order_spherical_harmonics(N): r"""Maximum order of 3D HOA. .. math:: - \text{max\_order} = \lfloor \sqrt{N} \rfloor - 1. + \mathtt{max\_order} = \lfloor \sqrt{N} \rfloor - 1. Parameters ---------- From b53442d4fbcb27b0899773f400b4f37ceb6d6a8f Mon Sep 17 00:00:00 2001 From: Hagen Wierstorf Date: Thu, 14 Mar 2019 14:05:15 +0100 Subject: [PATCH 060/101] Add intro how to build PDF and EBUP documentations --- CONTRIBUTING.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 480773c..006d7e4 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -40,11 +40,28 @@ To create the HTML pages, use:: The generated files will be available in the directory ``build/sphinx/html/``. +To create the EPUB file, use:: + + python3 setup.py build_sphinx -b epub + +The generated EPUB file will be available in the directory +``build/sphinx/epub/``. + +To create the PDF file, use:: + + python3 setup.py build_sphinx -b latex + +Afterwards go to the folder ``build/sphinx/latex/`` and run LaTeX to create the +PDF file. If you don’t know how to create a PDF file from the LaTeX output, you +should have a look at Latexmk_ (see also this `Latexmk tutorial`_). + It is also possible to automatically check if all links are still valid:: python3 setup.py build_sphinx -b linkcheck .. _Sphinx: http://sphinx-doc.org/ +.. _Latexmk: http://personal.psu.edu/jcc8/software/latexmk-jcc/ +.. _Latexmk tutorial: https://mg.readthedocs.io/latexmk.html Running the Tests ^^^^^^^^^^^^^^^^^ From a15b4f362989493597e7f381ed2d9ec23b1d08e3 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 13 Mar 2019 19:32:27 +0100 Subject: [PATCH 061/101] Change hypen-looking thing to an actual hyphen in bib file --- doc/references.bib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/references.bib b/doc/references.bib index d1c2c02..3b510c8 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -54,7 +54,7 @@ @phdthesis{Wierstorf2014 } @article{Allen1979, author = {Allen, J. B. and Berkley, D. A.}, - title = {{Image method for efficiently simulating small‐room acoustics}}, + title = {{Image method for efficiently simulating small-room acoustics}}, journal = {Journal of the Acoustical Society of America}, volume = {65}, pages = {943--950}, From c74e34a8b64f0c283be0624d2808e20ec70f0da5 Mon Sep 17 00:00:00 2001 From: Frank Schultz Date: Thu, 14 Mar 2019 20:36:46 +0100 Subject: [PATCH 062/101] new Point Source 2.5D WFS handling, old one becomes legacy -old handling was Spors/Rabenstein, 2D to 2.5D -new one is Delft, 3D to 2.5D --- doc/references.bib | 26 ++++++++++ sfs/mono/wfs.py | 115 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 135 insertions(+), 6 deletions(-) diff --git a/doc/references.bib b/doc/references.bib index 3b510c8..8b149ba 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -70,3 +70,29 @@ @article{Borish1984 year = {1984}, doi = {10.1121/1.390983} } +@article{Firtha2017, + author = {Gergely Firtha AND P{\'e}ter Fiala AND Frank Schultz AND + Sascha Spors}, + title = {{Improved Referencing Schemes for 2.5D Wave Field Synthesis + Driving Functions}}, + journal = {IEEE/ACM Trans. Audio Speech Language Process.}, + volume = {25}, + number = {5}, + pages = {1117-1127}, + year = {2017}, + doi = {10.1109/TASLP.2017.2689245} +} +@phdthesis{Start1997, + author = {Evert W. Start}, + title = {{Direct Sound Enhancement by Wave Field Synthesis}}, + school = {Delft University of Technology}, + year = {1997} +} +@phdthesis{Schultz2016, + author = {Frank Schultz}, + title = {{Sound Field Synthesis for Line Source Array Applications in + Large-Scale Sound Reinforcement}}, + school = {University of Rostock}, + year = {2016}, + doi = {10.18453/rosdok_id00001765} +} diff --git a/sfs/mono/wfs.py b/sfs/mono/wfs.py index a05aa0f..3e5762a 100644 --- a/sfs/mono/wfs.py +++ b/sfs/mono/wfs.py @@ -155,8 +155,102 @@ def _point(omega, x0, n0, xs, c=None): def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): + r"""Driving function for 2.5-dimensional WFS of a virtual point source. + + .. versionchanged:: 0.5 + see notes, old handling of `point_25d()` is now `point_25d_legacy()` + + Parameters + ---------- + omega : float + Angular frequency of point source. + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of normal vectors of secondary sources. + xs : (3,) array_like + Position of virtual point source. + xref : (3,) array_like, optional + Reference point xref or contour xref(x0) for amplitude correct + synthesis. + c : float, optional + Speed of sound in m/s. + omalias: float, optional + Angular frequency where spatial aliasing becomes prominent. + + Returns + ------- + d : (N,) numpy.ndarray + Complex weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.mono.synthesize()`. + + Notes + ----- + `point_25d()` derives 2.5D WFS from the 3D + Neumann-Rayleigh integral (i.e. the TU Delft approach). + The eq. (3.10), (3.11) in :cite:`Start1997`, equivalent to + Eq. (2.137) in :cite:`Schultz2016` + + .. math:: + + D(\x_0,\w) = \sqrt{8 \pi \, \i\wc} + \sqrt{\frac{|\x_\text{ref}-\x_0| \cdot + |\x_0-\x_\text{s}|}{|\x_\text{ref}-\x_0| + |\x_0-\x_\text{s}|}} + \scalarprod{\frac{\x_0-\x_\text{s}}{|\x_0-\x_\text{s}|}}{\n_0} + \frac{\e{-\i\wc |\x_0-\x_\text{s}|}}{4\pi\,|\x_0-\x_\text{s}|} + + is implemented. + The theoretical link of `point_25d()` and `point_25d_legacy()` was + introduced as *unified WFS framework* in :cite:`Firtha2017`. + + Examples + -------- + .. plot:: + :context: close-figs + + d, selection, secondary_source = sfs.mono.wfs.point_25d( + omega, array.x, array.n, xs) + normalize_gain = 4 * np.pi * np.linalg.norm(xs) + plot(normalize_gain * d, selection, secondary_source) + + """ + x0 = util.asarray_of_rows(x0) + n0 = util.asarray_of_rows(n0) + xs = util.asarray_1d(xs) + xref = util.asarray_1d(xref) + k = util.wavenumber(omega, c) + + ds = x0 - xs + dr = xref - x0 + s = np.linalg.norm(ds, axis=1) + r = np.linalg.norm(dr, axis=1) + + d = ( + preeq_25d(omega, omalias, c) * + np.sqrt(8 * np.pi) * + np.sqrt((r * s) / (r + s)) * + inner1d(n0, ds) / s * + np.exp(-1j * k * s) / (4 * np.pi * s)) + selection = util.source_selection_point(n0, x0, xs) + return d, selection, secondary_source_point(omega, c) + + +point_3d = _point + + +def point_25d_legacy(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): r"""Driving function for 2.5-dimensional WFS for a virtual point source. + .. versionadded:: 0.5 + `point_25d()` was renamed to `point_25d_legacy()` (and a new + function with the name `point_25d()` was introduced). See notes for + further details. + Parameters ---------- omega : float @@ -171,6 +265,8 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): Reference point for synthesized sound field. c : float, optional Speed of sound. + omalias: float, optional + Angular frequency where spatial aliasing becomes prominent. Returns ------- @@ -185,6 +281,10 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): Notes ----- + `point_25d_legacy()` derives 2.5D WFS from the 2D + Neumann-Rayleigh integral (i.e. the approach by Rabenstein & Spors), cf. + :cite:`Spors2008`. + .. math:: D(\x_0,\w) = \sqrt{\i\wc |\x_\text{ref}-\x_0|} @@ -192,14 +292,19 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): {|\x_0-\x_\text{s}|^\frac{3}{2}} \e{-\i\wc |\x_0-\x_\text{s}|} + The theoretical link of `point_25d()` and `point_25d_legacy()` was + introduced as *unified WFS framework* in :cite:`Firtha2017`. + Also cf. Eq. (2.145)-(2.147) :cite:`Schultz2016`. + Examples -------- .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.wfs.point_25d( + d, selection, secondary_source = sfs.mono.wfs.point_25d_legacy( omega, array.x, array.n, xs) - plot(d, selection, secondary_source) + normalize_gain = np.linalg.norm(xs) + plot(normalize_gain * d, selection, secondary_source) """ x0 = util.asarray_of_rows(x0) @@ -217,9 +322,6 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): return d, selection, secondary_source_point(omega, c) -point_3d = _point - - def _plane(omega, x0, n0, n=[0, 1, 0], c=None): r"""Driving function for 2/3-dimensional WFS for a virtual plane wave. @@ -499,7 +601,8 @@ def preeq_25d(omega, omalias, c): H(\w) = \begin{cases} \sqrt{\i \wc} & \text{for } \w \leq \w_\text{alias} \\ - \sqrt{\i \frac{\w_\text{alias}}{c}} & \text{for } \w > \w_\text{alias} + \sqrt{\i \frac{\w_\text{alias}}{c}} & + \text{for } \w > \w_\text{alias} \end{cases} """ From 5a6f5b4c965929630c0b0545d74a05bd72626588 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 14 Mar 2019 11:42:38 +0100 Subject: [PATCH 063/101] Change mono/time to fd/td --- .../animations-pulsating-sphere.ipynb | 4 +- doc/examples/animations_pulsating_sphere.py | 6 +-- doc/examples/horizontal_plane_arrays.py | 38 +++++++++---------- doc/examples/mirror-image-source-model.ipynb | 8 ++-- doc/examples/modal-room-acoustics.ipynb | 4 +- doc/examples/plot_particle_density.py | 6 +-- doc/examples/sound_field_synthesis.py | 24 ++++++------ doc/examples/soundfigures.py | 4 +- doc/examples/time_domain.py | 24 ++++++------ doc/examples/time_domain_nfchoa.py | 16 ++++---- sfs/__init__.py | 8 ++-- sfs/{mono => fd}/__init__.py | 4 +- sfs/{mono => fd}/esa.py | 10 ++--- sfs/{mono => fd}/nfchoa.py | 14 +++---- sfs/{mono => fd}/sdm.py | 18 ++++----- sfs/{mono => fd}/source.py | 18 ++++----- sfs/{mono => fd}/wfs.py | 38 +++++++++---------- sfs/{time => td}/__init__.py | 2 +- sfs/{time => td}/nfchoa.py | 20 +++++----- sfs/{time => td}/source.py | 4 +- sfs/{time => td}/wfs.py | 22 +++++------ 21 files changed, 146 insertions(+), 146 deletions(-) rename sfs/{mono => fd}/__init__.py (93%) rename sfs/{mono => fd}/esa.py (97%) rename sfs/{mono => fd}/nfchoa.py (93%) rename sfs/{mono => fd}/sdm.py (92%) rename sfs/{mono => fd}/source.py (97%) rename sfs/{mono => fd}/wfs.py (94%) rename sfs/{time => td}/__init__.py (98%) rename sfs/{time => td}/nfchoa.py (96%) rename sfs/{time => td}/source.py (97%) rename sfs/{time => td}/wfs.py (92%) diff --git a/doc/examples/animations-pulsating-sphere.ipynb b/doc/examples/animations-pulsating-sphere.ipynb index fcf6408..e9efc73 100644 --- a/doc/examples/animations-pulsating-sphere.ipynb +++ b/doc/examples/animations-pulsating-sphere.ipynb @@ -28,8 +28,8 @@ "particle velocity, and particle displacement, are simulated.\n", "The first two quantities are computed with\n", "\n", - "- [sfs.mono.source.pulsating_sphere()](../sfs.mono.source.rst#sfs.mono.source.pulsating_sphere) and \n", - "- [sfs.mono.source.pulsating_sphere_velocity()](../sfs.mono.source.rst#sfs.mono.source.pulsating_sphere_velocity)\n", + "- [sfs.fd.source.pulsating_sphere()](../sfs.fd.source.rst#sfs.fd.source.pulsating_sphere) and \n", + "- [sfs.fd.source.pulsating_sphere_velocity()](../sfs.fd.source.rst#sfs.fd.source.pulsating_sphere_velocity)\n", "\n", "while the last one can be obtained by using\n", "\n", diff --git a/doc/examples/animations_pulsating_sphere.py b/doc/examples/animations_pulsating_sphere.py index 712d4fa..2257c48 100644 --- a/doc/examples/animations_pulsating_sphere.py +++ b/doc/examples/animations_pulsating_sphere.py @@ -8,7 +8,7 @@ def particle_displacement(omega, center, radius, amplitude, grid, frames, figsize=(8, 8), interval=80, blit=True, **kwargs): """Generate sound particle animation.""" - velocity = sfs.mono.source.pulsating_sphere_velocity( + velocity = sfs.fd.source.pulsating_sphere_velocity( omega, center, radius, amplitude, grid) displacement = sfs.util.displacement(velocity, omega) phasor = np.exp(1j * 2 * np.pi / frames) @@ -32,7 +32,7 @@ def update_frame_displacement(i): def particle_velocity(omega, center, radius, amplitude, grid, frames, figsize=(8, 8), interval=80, blit=True, **kwargs): """Generate particle velocity animation.""" - velocity = sfs.mono.source.pulsating_sphere_velocity( + velocity = sfs.fd.source.pulsating_sphere_velocity( omega, center, radius, amplitude, grid) phasor = np.exp(1j * 2 * np.pi / frames) @@ -54,7 +54,7 @@ def sound_pressure(omega, center, radius, amplitude, grid, frames, pulsate=False, figsize=(8, 8), interval=80, blit=True, **kwargs): """Generate sound pressure animation.""" - pressure = sfs.mono.source.pulsating_sphere( + pressure = sfs.fd.source.pulsating_sphere( omega, center, radius, amplitude, grid, inside=pulsate) phasor = np.exp(1j * 2 * np.pi / frames) diff --git a/doc/examples/horizontal_plane_arrays.py b/doc/examples/horizontal_plane_arrays.py index c2abeab..3cb677a 100644 --- a/doc/examples/horizontal_plane_arrays.py +++ b/doc/examples/horizontal_plane_arrays.py @@ -30,7 +30,7 @@ def compute_and_plot_soundfield(title): print('Computing', title) twin = tapering(selection, talpha) - p = sfs.mono.synthesize(d, twin, array, secondary_source, grid=grid) + p = sfs.fd.synthesize(d, twin, array, secondary_source, grid=grid) plt.figure(figsize=(15, 15)) plt.cla() @@ -46,34 +46,34 @@ def compute_and_plot_soundfield(title): # linear array, secondary point sources, virtual monopole array = sfs.array.linear(N, dx, center=acenter, orientation=anormal) -d, selection, secondary_source = sfs.mono.wfs.point_3d( +d, selection, secondary_source = sfs.fd.wfs.point_3d( omega, array.x, array.n, xs) compute_and_plot_soundfield('linear_ps_wfs_3d_point') -d, selection, secondary_source = sfs.mono.wfs.point_25d( +d, selection, secondary_source = sfs.fd.wfs.point_25d( omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('linear_ps_wfs_25d_point') -d, selection, secondary_source = sfs.mono.wfs.point_2d( +d, selection, secondary_source = sfs.fd.wfs.point_2d( omega, array.x, array.n, xs) compute_and_plot_soundfield('linear_ps_wfs_2d_point') # linear array, secondary line sources, virtual line source -d, selection, secondary_source = sfs.mono.wfs.line_2d( +d, selection, secondary_source = sfs.fd.wfs.line_2d( omega, array.x, array.n, xs) compute_and_plot_soundfield('linear_ls_wfs_2d_line') # linear array, secondary point sources, virtual plane wave -d, selection, secondary_source = sfs.mono.wfs.plane_3d( +d, selection, secondary_source = sfs.fd.wfs.plane_3d( omega, array.x, array.n, npw) compute_and_plot_soundfield('linear_ps_wfs_3d_plane') -d, selection, secondary_source = sfs.mono.wfs.plane_25d( +d, selection, secondary_source = sfs.fd.wfs.plane_25d( omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('linear_ps_wfs_25d_plane') -d, selection, secondary_source = sfs.mono.wfs.plane_2d( +d, selection, secondary_source = sfs.fd.wfs.plane_2d( omega, array.x, array.n, npw) compute_and_plot_soundfield('linear_ps_wfs_2d_plane') @@ -82,11 +82,11 @@ def compute_and_plot_soundfield(title): array = sfs.array.linear_diff(N//3 * [dx] + N//3 * [dx/2] + N//3 * [dx], center=acenter, orientation=anormal) -d, selection, secondary_source = sfs.mono.wfs.point_25d( +d, selection, secondary_source = sfs.fd.wfs.point_25d( omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('linear_nested_ps_wfs_25d_point') -d, selection, secondary_source = sfs.mono.wfs.plane_25d( +d, selection, secondary_source = sfs.fd.wfs.plane_25d( omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('linear_nested_ps_wfs_25d_plane') @@ -95,22 +95,22 @@ def compute_and_plot_soundfield(title): array = sfs.array.linear_random(N, dx/2, 1.5*dx, center=acenter, orientation=anormal) -d, selection, secondary_source = sfs.mono.wfs.point_25d( +d, selection, secondary_source = sfs.fd.wfs.point_25d( omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('linear_random_ps_wfs_25d_point') -d, selection, secondary_source = sfs.mono.wfs.plane_25d( +d, selection, secondary_source = sfs.fd.wfs.plane_25d( omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('linear_random_ps_wfs_25d_plane') # rectangular array, secondary point sources array = sfs.array.rectangular((N, N//2), dx, center=acenter, orientation=anormal) -d, selection, secondary_source = sfs.mono.wfs.point_25d( +d, selection, secondary_source = sfs.fd.wfs.point_25d( omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('rectangular_ps_wfs_25d_point') -d, selection, secondary_source = sfs.mono.wfs.plane_25d( +d, selection, secondary_source = sfs.fd.wfs.plane_25d( omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('rectangular_ps_wfs_25d_plane') @@ -118,11 +118,11 @@ def compute_and_plot_soundfield(title): # circular array, secondary point sources N = 60 array = sfs.array.circular(N, 1, center=acenter) -d, selection, secondary_source = sfs.mono.wfs.point_25d( +d, selection, secondary_source = sfs.fd.wfs.point_25d( omega, array.x, array.n, xs, xref=xnorm) compute_and_plot_soundfield('circular_ps_wfs_25d_point') -d, selection, secondary_source = sfs.mono.wfs.plane_25d( +d, selection, secondary_source = sfs.fd.wfs.plane_25d( omega, array.x, array.n, npw, xref=xnorm) compute_and_plot_soundfield('circular_ps_wfs_25d_plane') @@ -132,7 +132,7 @@ def compute_and_plot_soundfield(title): xnorm = [0, 0, 0] talpha = 0 # switches off tapering -d, selection, secondary_source = sfs.mono.nfchoa.plane_2d( +d, selection, secondary_source = sfs.fd.nfchoa.plane_2d( omega, array.x, 1, npw) compute_and_plot_soundfield('circular_ls_nfchoa_2d_plane') @@ -142,10 +142,10 @@ def compute_and_plot_soundfield(title): xnorm = [0, 0, 0] talpha = 0 # switches off tapering -d, selection, secondary_source = sfs.mono.nfchoa.point_25d( +d, selection, secondary_source = sfs.fd.nfchoa.point_25d( omega, array.x, 1, xs) compute_and_plot_soundfield('circular_ps_nfchoa_25d_point') -d, selection, secondary_source = sfs.mono.nfchoa.plane_25d( +d, selection, secondary_source = sfs.fd.nfchoa.plane_25d( omega, array.x, 1, npw) compute_and_plot_soundfield('circular_ps_nfchoa_25d_plane') diff --git a/doc/examples/mirror-image-source-model.ipynb b/doc/examples/mirror-image-source-model.ipynb index a20e93f..7b9afa0 100644 --- a/doc/examples/mirror-image-source-model.ipynb +++ b/doc/examples/mirror-image-source-model.ipynb @@ -93,8 +93,8 @@ "outputs": [], "source": [ "grid = sfs.util.xyz_grid([0, L[0]], [0, L[1]], 1.5, spacing=0.02)\n", - "P = sfs.mono.source.point_image_sources(omega, x0, grid, L,\n", - " max_order, coeffs=coeffs)" + "P = sfs.fd.source.point_image_sources(omega, x0, grid, L,\n", + " max_order, coeffs=coeffs)" ] }, { @@ -130,8 +130,8 @@ "outputs": [], "source": [ "grid = sfs.util.xyz_grid([0, L[0]], [0, L[1]], 1.5, spacing=0.005)\n", - "p = sfs.time.source.point_image_sources(x0, signal, 0.004, grid, L, max_order,\n", - " coeffs=coeffs)" + "p = sfs.td.source.point_image_sources(x0, signal, 0.004, grid, L, max_order,\n", + " coeffs=coeffs)" ] }, { diff --git a/doc/examples/modal-room-acoustics.ipynb b/doc/examples/modal-room-acoustics.ipynb index 6427119..ef7adf9 100644 --- a/doc/examples/modal-room-acoustics.ipynb +++ b/doc/examples/modal-room-acoustics.ipynb @@ -87,7 +87,7 @@ "metadata": {}, "outputs": [], "source": [ - "p = sfs.mono.source.point_modal(omega, x0, grid, L, N=N, deltan=deltan)" + "p = sfs.fd.source.point_modal(omega, x0, grid, L, N=N, deltan=deltan)" ] }, { @@ -135,7 +135,7 @@ "\n", "receiver = 1, 1, 1.8\n", "\n", - "p = [sfs.mono.source.point_modal(om, x0, receiver, L, N=N, deltan=deltan)\n", + "p = [sfs.fd.source.point_modal(om, x0, receiver, L, N=N, deltan=deltan)\n", " for om in omega]\n", " \n", "plt.plot(f, sfs.util.db(p))\n", diff --git a/doc/examples/plot_particle_density.py b/doc/examples/plot_particle_density.py index f5b15e3..6f93f74 100644 --- a/doc/examples/plot_particle_density.py +++ b/doc/examples/plot_particle_density.py @@ -31,16 +31,16 @@ def plot_particle_displacement(title): # point source -v = sfs.mono.source.point_velocity(omega, xs, grid) +v = sfs.fd.source.point_velocity(omega, xs, grid) amplitude = 1.5e6 plot_particle_displacement('particle_displacement_point_source') # line source -v = sfs.mono.source.line_velocity(omega, xs, grid) +v = sfs.fd.source.line_velocity(omega, xs, grid) amplitude = 1.3e6 plot_particle_displacement('particle_displacement_line_source') # plane wave -v = sfs.mono.source.plane_velocity(omega, xs, npw, grid) +v = sfs.fd.source.plane_velocity(omega, xs, npw, grid) amplitude = 1e5 plot_particle_displacement('particle_displacement_plane_wave') diff --git a/doc/examples/sound_field_synthesis.py b/doc/examples/sound_field_synthesis.py index 81cd8be..55d2cf8 100644 --- a/doc/examples/sound_field_synthesis.py +++ b/doc/examples/sound_field_synthesis.py @@ -44,22 +44,22 @@ # === compute driving function and determine active secondary sources === -#d, selection, secondary_source = sfs.mono.wfs.plane_3d_delay(omega, array.x, array.n, npw) +#d, selection, secondary_source = sfs.fd.wfs.plane_3d_delay(omega, array.x, array.n, npw) -#d, selection, secondary_source = sfs.mono.wfs.line_2d(omega, array.x, array.n, xs) +#d, selection, secondary_source = sfs.fd.wfs.line_2d(omega, array.x, array.n, xs) -#d, selection, secondary_source = sfs.mono.wfs.plane_2d(omega, array.x, array.n, npw) -d, selection, secondary_source = sfs.mono.wfs.plane_25d(omega, array.x, array.n, npw, xref) -#d, selection, secondary_source = sfs.mono.wfs.plane_3d(omega, array.x, array.n, npw) +#d, selection, secondary_source = sfs.fd.wfs.plane_2d(omega, array.x, array.n, npw) +d, selection, secondary_source = sfs.fd.wfs.plane_25d(omega, array.x, array.n, npw, xref) +#d, selection, secondary_source = sfs.fd.wfs.plane_3d(omega, array.x, array.n, npw) -#d, selection, secondary_source = sfs.mono.wfs.point_2d(omega, array.x, array.n, xs) -#d, selection, secondary_source = sfs.mono.wfs.point_25d(omega, array.x, array.n, xs) -#d, selection, secondary_source = sfs.mono.wfs.point_3d(omega, array.x, array.n, xs) +#d, selection, secondary_source = sfs.fd.wfs.point_2d(omega, array.x, array.n, xs) +#d, selection, secondary_source = sfs.fd.wfs.point_25d(omega, array.x, array.n, xs) +#d, selection, secondary_source = sfs.fd.wfs.point_3d(omega, array.x, array.n, xs) -#d, selection, secondary_source = sfs.mono.nfchoa.plane_2d(omega, array.x, R, npw) +#d, selection, secondary_source = sfs.fd.nfchoa.plane_2d(omega, array.x, R, npw) -#d, selection, secondary_source = sfs.mono.nfchoa.point_25d(omega, array.x, R, xs) -#d, selection, secondary_source = sfs.mono.nfchoa.plane_25d(omega, array.x, R, npw) +#d, selection, secondary_source = sfs.fd.nfchoa.point_25d(omega, array.x, R, xs) +#d, selection, secondary_source = sfs.fd.nfchoa.plane_25d(omega, array.x, R, npw) # === compute tapering window === @@ -68,7 +68,7 @@ twin = sfs.tapering.tukey(selection, 0.3) # === compute synthesized sound field === -p = sfs.mono.synthesize(d, twin, array, secondary_source, grid=grid) +p = sfs.fd.synthesize(d, twin, array, secondary_source, grid=grid) # === plot synthesized sound field === diff --git a/doc/examples/soundfigures.py b/doc/examples/soundfigures.py index 9405182..7260db6 100644 --- a/doc/examples/soundfigures.py +++ b/doc/examples/soundfigures.py @@ -29,11 +29,11 @@ # driving function for sound figure figure = np.array(Image.open('figures/tree.png')) # read image from file figure = np.rot90(figure) # turn 0deg to the top -d, selection, secondary_source = sfs.mono.wfs.soundfigure_3d( +d, selection, secondary_source = sfs.fd.wfs.soundfigure_3d( omega, array.x, array.n, figure, npw=npw) # compute synthesized sound field -p = sfs.mono.synthesize(d, selection, array, secondary_source, grid=grid) +p = sfs.fd.synthesize(d, selection, array, secondary_source, grid=grid) # plot and save synthesized sound field plt.figure(figsize=(10, 10)) diff --git a/doc/examples/time_domain.py b/doc/examples/time_domain.py index 8f64d9a..79d965b 100644 --- a/doc/examples/time_domain.py +++ b/doc/examples/time_domain.py @@ -25,14 +25,14 @@ t = 0.008 # compute driving signals d_delay, d_weight, selection, secondary_source = \ - sfs.time.wfs.point_25d(array.x, array.n, xs) -d = sfs.time.wfs.driving_signals(d_delay, d_weight, signal) + sfs.td.wfs.point_25d(array.x, array.n, xs) +d = sfs.td.wfs.driving_signals(d_delay, d_weight, signal) # test soundfield twin = sfs.tapering.tukey(selection, .3) -p = sfs.time.synthesize(d, twin, array, - secondary_source, observation_time=t, grid=grid) +p = sfs.td.synthesize(d, twin, array, + secondary_source, observation_time=t, grid=grid) p = p * 100 # scale absolute amplitude plt.figure(figsize=(10, 10)) @@ -50,13 +50,13 @@ # compute driving signals d_delay, d_weight, selection, secondary_source = \ - sfs.time.wfs.plane_25d(array.x, array.n, npw) -d = sfs.time.wfs.driving_signals(d_delay, d_weight, signal) + sfs.td.wfs.plane_25d(array.x, array.n, npw) +d = sfs.td.wfs.driving_signals(d_delay, d_weight, signal) # test soundfield twin = sfs.tapering.tukey(selection, .3) -p = sfs.time.synthesize(d, twin, array, - secondary_source, observation_time=t, grid=grid) +p = sfs.td.synthesize(d, twin, array, + secondary_source, observation_time=t, grid=grid) plt.figure(figsize=(10, 10)) sfs.plot.level(p, grid, cmap=my_cmap) @@ -72,13 +72,13 @@ nfs = sfs.util.normalize_vector(xref - xs) # main n of fsource t = 0.003 # compute driving signals d_delay, d_weight, selection, secondary_source = \ - sfs.time.wfs.focused_25d(array.x, array.n, xs, nfs) -d = sfs.time.wfs.driving_signals(d_delay, d_weight, signal) + sfs.td.wfs.focused_25d(array.x, array.n, xs, nfs) +d = sfs.td.wfs.driving_signals(d_delay, d_weight, signal) # test soundfield twin = sfs.tapering.tukey(selection, .3) -p = sfs.time.synthesize(d, twin, array, - secondary_source, observation_time=t, grid=grid) +p = sfs.td.synthesize(d, twin, array, + secondary_source, observation_time=t, grid=grid) p = p * 100 # scale absolute amplitude plt.figure(figsize=(10, 10)) diff --git a/doc/examples/time_domain_nfchoa.py b/doc/examples/time_domain_nfchoa.py index 204f95e..c884832 100644 --- a/doc/examples/time_domain_nfchoa.py +++ b/doc/examples/time_domain_nfchoa.py @@ -20,11 +20,11 @@ npw = [0, -1, 0] # propagating direction t = 0 # observation time delay, weight, sos, phaseshift, selection, secondary_source = \ - sfs.time.nfchoa.plane_25d(array.x, R, npw, fs, max_order) -d = sfs.time.nfchoa.driving_signals_25d( + sfs.td.nfchoa.plane_25d(array.x, R, npw, fs, max_order) +d = sfs.td.nfchoa.driving_signals_25d( delay, weight, sos, phaseshift, signal) -p = sfs.time.synthesize(d, selection, array, secondary_source, - observation_time=t, grid=grid) +p = sfs.td.synthesize(d, selection, array, secondary_source, + observation_time=t, grid=grid) plt.figure() sfs.plot.level(p, grid) @@ -37,11 +37,11 @@ xs = [1.5, 1.5, 0] # position t = np.linalg.norm(xs) / sfs.default.c # observation time delay, weight, sos, phaseshift, selection, secondary_source = \ - sfs.time.nfchoa.point_25d(array.x, R, xs, fs, max_order) -d = sfs.time.nfchoa.driving_signals_25d( + sfs.td.nfchoa.point_25d(array.x, R, xs, fs, max_order) +d = sfs.td.nfchoa.driving_signals_25d( delay, weight, sos, phaseshift, signal) -p = sfs.time.synthesize(d, selection, array, secondary_source, - observation_time=t, grid=grid) +p = sfs.td.synthesize(d, selection, array, secondary_source, + observation_time=t, grid=grid) plt.figure() sfs.plot.level(p, grid) diff --git a/sfs/__init__.py b/sfs/__init__.py index 8271b28..5ea1dcf 100644 --- a/sfs/__init__.py +++ b/sfs/__init__.py @@ -7,10 +7,10 @@ .. autosummary:: :toctree: + fd + td array tapering - mono - time plot util @@ -63,5 +63,5 @@ def reset(self): except ImportError: pass -from . import mono -from . import time +from . import fd +from . import td diff --git a/sfs/mono/__init__.py b/sfs/fd/__init__.py similarity index 93% rename from sfs/mono/__init__.py rename to sfs/fd/__init__.py index 6961969..ad3c0cc 100644 --- a/sfs/mono/__init__.py +++ b/sfs/fd/__init__.py @@ -60,7 +60,7 @@ def synthesize(d, weights, ssd, secondary_source_function, **kwargs): def secondary_source_point(omega, c): - """Create a point source for use in `sfs.mono.synthesize()`.""" + """Create a point source for use in `sfs.fd.synthesize()`.""" def secondary_source(position, _, grid): return source.point(omega, position, grid, c) @@ -69,7 +69,7 @@ def secondary_source(position, _, grid): def secondary_source_line(omega, c): - """Create a line source for use in `sfs.mono.synthesize()`.""" + """Create a line source for use in `sfs.fd.synthesize()`.""" def secondary_source(position, _, grid): return source.line(omega, position, grid, c) diff --git a/sfs/mono/esa.py b/sfs/fd/esa.py similarity index 97% rename from sfs/mono/esa.py rename to sfs/fd/esa.py index 5584761..516acc7 100644 --- a/sfs/mono/esa.py +++ b/sfs/fd/esa.py @@ -49,7 +49,7 @@ def plane_2d_edge(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -120,7 +120,7 @@ def plane_2d_edge_dipole_ssd(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -187,7 +187,7 @@ def line_2d_edge(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -261,7 +261,7 @@ def line_2d_edge_dipole_ssd(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -335,7 +335,7 @@ def point_25d_edge(omega, x0, xs, xref=[2, -2, 0], alpha=3/2*np.pi, whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- diff --git a/sfs/mono/nfchoa.py b/sfs/fd/nfchoa.py similarity index 93% rename from sfs/mono/nfchoa.py rename to sfs/fd/nfchoa.py index 6677d22..2a8312b 100644 --- a/sfs/mono/nfchoa.py +++ b/sfs/fd/nfchoa.py @@ -23,7 +23,7 @@ array = sfs.array.circular(N=32, R=R) def plot(d, selection, secondary_source): - p = sfs.mono.synthesize(d, selection, array, secondary_source, grid=grid) + p = sfs.fd.synthesize(d, selection, array, secondary_source, grid=grid) sfs.plot.soundfield(p, grid) sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) @@ -62,7 +62,7 @@ def plane_2d(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): all secondary source are "active" for NFC-HOA. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -81,7 +81,7 @@ def plane_2d(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.nfchoa.plane_2d( + d, selection, secondary_source = sfs.fd.nfchoa.plane_2d( omega, array.x, R, npw) plot(d, selection, secondary_source) @@ -128,7 +128,7 @@ def point_25d(omega, x0, r0, xs, max_order=None, c=None): all secondary source are "active" for NFC-HOA. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -147,7 +147,7 @@ def point_25d(omega, x0, r0, xs, max_order=None, c=None): .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.nfchoa.point_25d( + d, selection, secondary_source = sfs.fd.nfchoa.point_25d( omega, array.x, R, xs) plot(d, selection, secondary_source) @@ -196,7 +196,7 @@ def plane_25d(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): all secondary source are "active" for NFC-HOA. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -215,7 +215,7 @@ def plane_25d(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.nfchoa.plane_25d( + d, selection, secondary_source = sfs.fd.nfchoa.plane_25d( omega, array.x, R, npw) plot(d, selection, secondary_source) diff --git a/sfs/mono/sdm.py b/sfs/fd/sdm.py similarity index 92% rename from sfs/mono/sdm.py rename to sfs/fd/sdm.py index 2d775ee..b3af25f 100644 --- a/sfs/mono/sdm.py +++ b/sfs/fd/sdm.py @@ -22,7 +22,7 @@ array = sfs.array.linear(32, 0.2, orientation=[0, -1, 0]) def plot(d, selection, secondary_source): - p = sfs.mono.synthesize(d, selection, array, secondary_source, grid=grid) + p = sfs.fd.synthesize(d, selection, array, secondary_source, grid=grid) sfs.plot.soundfield(p, grid) sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) @@ -59,7 +59,7 @@ def line_2d(omega, x0, n0, xs, c=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -71,7 +71,7 @@ def line_2d(omega, x0, n0, xs, c=None): .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.sdm.line_2d( + d, selection, secondary_source = sfs.fd.sdm.line_2d( omega, array.x, array.n, xs) plot(d, selection, secondary_source) @@ -112,7 +112,7 @@ def plane_2d(omega, x0, n0, n=[0, 1, 0], c=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -128,7 +128,7 @@ def plane_2d(omega, x0, n0, n=[0, 1, 0], c=None): .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.sdm.plane_2d( + d, selection, secondary_source = sfs.fd.sdm.plane_2d( omega, array.x, array.n, npw) plot(d, selection, secondary_source) @@ -169,7 +169,7 @@ def plane_25d(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -181,7 +181,7 @@ def plane_25d(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.sdm.plane_25d( + d, selection, secondary_source = sfs.fd.sdm.plane_25d( omega, array.x, array.n, npw, [0, -1, 0]) plot(d, selection, secondary_source) @@ -224,7 +224,7 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -236,7 +236,7 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None): .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.sdm.point_25d( + d, selection, secondary_source = sfs.fd.sdm.point_25d( omega, array.x, array.n, xs, [0, -1, 0]) plot(d, selection, secondary_source) diff --git a/sfs/mono/source.py b/sfs/fd/source.py similarity index 97% rename from sfs/mono/source.py rename to sfs/fd/source.py index e3d808e..c7581a9 100644 --- a/sfs/mono/source.py +++ b/sfs/fd/source.py @@ -63,7 +63,7 @@ def point(omega, x0, grid, c=None): .. plot:: :context: close-figs - p = sfs.mono.source.point(omega, x0, grid) + p = sfs.fd.source.point(omega, x0, grid) sfs.plot.soundfield(p, grid) plt.title("Point Source at {} m".format(x0)) @@ -114,7 +114,7 @@ def point_velocity(omega, x0, grid, c=None, rho0=None): .. plot:: :context: close-figs - v = sfs.mono.source.point_velocity(omega, x0, vgrid) + v = sfs.fd.source.point_velocity(omega, x0, vgrid) sfs.plot.soundfield(p * normalization_point, grid) sfs.plot.vectors(v * normalization_point, vgrid) plt.title("Sound Pressure and Particle Velocity") @@ -205,7 +205,7 @@ def point_dipole(omega, x0, n0, grid, c=None): :context: close-figs n0 = 0, 1, 0 - p = sfs.mono.source.point_dipole(omega, x0, n0, grid) + p = sfs.fd.source.point_dipole(omega, x0, n0, grid) sfs.plot.soundfield(p, grid) plt.title("Dipole Point Source at {} m".format(x0)) @@ -416,7 +416,7 @@ def line(omega, x0, grid, c=None): .. plot:: :context: close-figs - p = sfs.mono.source.line(omega, x0, grid) + p = sfs.fd.source.line(omega, x0, grid) sfs.plot.soundfield(p, grid) plt.title("Line Source at {} m".format(x0[:2])) @@ -454,7 +454,7 @@ def line_velocity(omega, x0, grid, c=None, rho0=None): .. plot:: :context: close-figs - v = sfs.mono.source.line_velocity(omega, x0, vgrid) + v = sfs.fd.source.line_velocity(omega, x0, vgrid) sfs.plot.soundfield(p * normalization_line, grid) sfs.plot.vectors(v * normalization_line, vgrid) plt.title("Sound Pressure and Particle Velocity") @@ -605,7 +605,7 @@ def plane(omega, x0, n0, grid, c=None): direction = 45 # degree n0 = sfs.util.direction_vector(np.radians(direction)) - p = sfs.mono.source.plane(omega, x0, n0, grid) + p = sfs.fd.source.plane(omega, x0, n0, grid) sfs.plot.soundfield(p, grid, colorbar_kwargs=dict(label="p / Pa")) plt.title("Plane wave with direction {} degree".format(direction)) @@ -654,7 +654,7 @@ def plane_velocity(omega, x0, n0, grid, c=None, rho0=None): .. plot:: :context: close-figs - v = sfs.mono.source.plane_velocity(omega, x0, n0, vgrid) + v = sfs.fd.source.plane_velocity(omega, x0, n0, vgrid) sfs.plot.soundfield(p, grid) sfs.plot.vectors(v, vgrid) plt.title("Sound Pressure and Particle Velocity") @@ -744,7 +744,7 @@ def pulsating_sphere(omega, center, radius, amplitude, grid, inside=False, radius = 0.25 amplitude = 1 / (radius * omega * sfs.default.rho0 * sfs.default.c) - p = sfs.mono.source.pulsating_sphere(omega, x0, radius, amplitude, grid) + p = sfs.fd.source.pulsating_sphere(omega, x0, radius, amplitude, grid) sfs.plot.soundfield(p, grid) plt.title("Sound Pressure of a Pulsating Sphere") @@ -796,7 +796,7 @@ def pulsating_sphere_velocity(omega, center, radius, amplitude, grid, c=None): .. plot:: :context: close-figs - v = sfs.mono.source.pulsating_sphere_velocity(omega, x0, radius, amplitude, vgrid) + v = sfs.fd.source.pulsating_sphere_velocity(omega, x0, radius, amplitude, vgrid) sfs.plot.soundfield(p, grid) sfs.plot.vectors(v, vgrid) plt.title("Sound Pressure and Particle Velocity of a Pulsating Sphere") diff --git a/sfs/mono/wfs.py b/sfs/fd/wfs.py similarity index 94% rename from sfs/mono/wfs.py rename to sfs/fd/wfs.py index 3e5762a..32e805d 100644 --- a/sfs/mono/wfs.py +++ b/sfs/fd/wfs.py @@ -26,7 +26,7 @@ array = sfs.array.circular(N=32, R=R) def plot(d, selection, secondary_source): - p = sfs.mono.synthesize(d, selection, array, secondary_source, grid=grid) + p = sfs.fd.synthesize(d, selection, array, secondary_source, grid=grid) sfs.plot.soundfield(p, grid) sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) @@ -64,7 +64,7 @@ def line_2d(omega, x0, n0, xs, c=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -79,7 +79,7 @@ def line_2d(omega, x0, n0, xs, c=None): .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.wfs.line_2d( + d, selection, secondary_source = sfs.fd.wfs.line_2d( omega, array.x, array.n, xs) plot(d, selection, secondary_source) @@ -120,7 +120,7 @@ def _point(omega, x0, n0, xs, c=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -135,7 +135,7 @@ def _point(omega, x0, n0, xs, c=None): .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.wfs.point_3d( + d, selection, secondary_source = sfs.fd.wfs.point_3d( omega, array.x, array.n, xs) plot(d, selection, secondary_source) @@ -187,7 +187,7 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -213,7 +213,7 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.wfs.point_25d( + d, selection, secondary_source = sfs.fd.wfs.point_25d( omega, array.x, array.n, xs) normalize_gain = 4 * np.pi * np.linalg.norm(xs) plot(normalize_gain * d, selection, secondary_source) @@ -277,7 +277,7 @@ def point_25d_legacy(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -301,7 +301,7 @@ def point_25d_legacy(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.wfs.point_25d_legacy( + d, selection, secondary_source = sfs.fd.wfs.point_25d_legacy( omega, array.x, array.n, xs) normalize_gain = np.linalg.norm(xs) plot(normalize_gain * d, selection, secondary_source) @@ -347,7 +347,7 @@ def _plane(omega, x0, n0, n=[0, 1, 0], c=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -363,7 +363,7 @@ def _plane(omega, x0, n0, n=[0, 1, 0], c=None): .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.wfs.plane_3d( + d, selection, secondary_source = sfs.fd.wfs.plane_3d( omega, array.x, array.n, npw) plot(d, selection, secondary_source) @@ -410,7 +410,7 @@ def plane_25d(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -425,7 +425,7 @@ def plane_25d(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.wfs.plane_25d( + d, selection, secondary_source = sfs.fd.wfs.plane_25d( omega, array.x, array.n, npw) plot(d, selection, secondary_source) @@ -473,7 +473,7 @@ def _focused(omega, x0, n0, xs, ns, c=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -488,7 +488,7 @@ def _focused(omega, x0, n0, xs, ns, c=None): .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.wfs.focused_3d( + d, selection, secondary_source = sfs.fd.wfs.focused_3d( omega, array.x, array.n, xs_focused, ns_focused) plot(d, selection, secondary_source) @@ -539,7 +539,7 @@ def focused_25d(omega, x0, n0, xs, ns, xref=[0, 0, 0], c=None, whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -555,7 +555,7 @@ def focused_25d(omega, x0, n0, xs, ns, xref=[0, 0, 0], c=None, .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.wfs.focused_25d( + d, selection, secondary_source = sfs.fd.wfs.focused_25d( omega, array.x, array.n, xs_focused, ns_focused) plot(d, selection, secondary_source) @@ -640,7 +640,7 @@ def plane_3d_delay(omega, x0, n0, n=[0, 1, 0], c=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.mono.synthesize()`. + single secondary source. See `sfs.fd.synthesize()`. Notes ----- @@ -653,7 +653,7 @@ def plane_3d_delay(omega, x0, n0, n=[0, 1, 0], c=None): .. plot:: :context: close-figs - d, selection, secondary_source = sfs.mono.wfs.plane_3d_delay( + d, selection, secondary_source = sfs.fd.wfs.plane_3d_delay( omega, array.x, array.n, npw) plot(d, selection, secondary_source) diff --git a/sfs/time/__init__.py b/sfs/td/__init__.py similarity index 98% rename from sfs/time/__init__.py rename to sfs/td/__init__.py index 15ce5b4..6770b5c 100644 --- a/sfs/time/__init__.py +++ b/sfs/td/__init__.py @@ -100,7 +100,7 @@ def apply_delays(signal, delays): def secondary_source_point(c): - """Create a point source for use in `sfs.time.synthesize()`.""" + """Create a point source for use in `sfs.td.synthesize()`.""" def secondary_source(position, _, signal, observation_time, grid): return source.point(position, signal, observation_time, grid, c=c) diff --git a/sfs/time/nfchoa.py b/sfs/td/nfchoa.py similarity index 96% rename from sfs/time/nfchoa.py rename to sfs/td/nfchoa.py index cbdf2ba..74981b0 100644 --- a/sfs/time/nfchoa.py +++ b/sfs/td/nfchoa.py @@ -30,8 +30,8 @@ grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02) def plot(d, selection, secondary_source, t=0): - p = sfs.time.synthesize(d, selection, array, secondary_source, grid=grid, - observation_time=t) + p = sfs.td.synthesize(d, selection, array, secondary_source, grid=grid, + observation_time=t) sfs.plot.level(p, grid) sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) @@ -136,7 +136,7 @@ def plane_25d(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk): all secondary source are "active" for NFC-HOA. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.time.synthesize()`. + single secondary source. See `sfs.td.synthesize()`. Examples -------- @@ -144,8 +144,8 @@ def plane_25d(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk): :context: close-figs delay, weight, sos, phaseshift, selection, secondary_source = \ - sfs.time.nfchoa.plane_25d(array.x, R, npw, fs) - d = sfs.time.nfchoa.driving_signals_25d( + sfs.td.nfchoa.plane_25d(array.x, R, npw, fs) + d = sfs.td.nfchoa.driving_signals_25d( delay, weight, sos, phaseshift, signal) plot(d, selection, secondary_source) @@ -232,7 +232,7 @@ def point_25d(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): all secondary source are "active" for NFC-HOA. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.time.synthesize()`. + single secondary source. See `sfs.td.synthesize()`. Examples -------- @@ -240,8 +240,8 @@ def point_25d(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): :context: close-figs delay, weight, sos, phaseshift, selection, secondary_source = \ - sfs.time.nfchoa.point_25d(array.x, R, xs, fs) - d = sfs.time.nfchoa.driving_signals_25d( + sfs.td.nfchoa.point_25d(array.x, R, xs, fs) + d = sfs.td.nfchoa.driving_signals_25d( delay, weight, sos, phaseshift, signal) plot(d, selection, secondary_source, t=ts) @@ -329,7 +329,7 @@ def plane_3d(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk): all secondary source are "active" for NFC-HOA. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.time.synthesize()`. + single secondary source. See `sfs.td.synthesize()`. """ if max_order is None: @@ -416,7 +416,7 @@ def point_3d(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): all secondary source are "active" for NFC-HOA. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.time.synthesize()`. + single secondary source. See `sfs.td.synthesize()`. """ if max_order is None: diff --git a/sfs/time/source.py b/sfs/td/source.py similarity index 97% rename from sfs/time/source.py rename to sfs/td/source.py index cc0154e..7d915c1 100644 --- a/sfs/time/source.py +++ b/sfs/td/source.py @@ -68,7 +68,7 @@ def point(xs, signal, observation_time, grid, c=None): .. plot:: :context: close-figs - p = sfs.time.source.point(xs, signal, ts, grid) + p = sfs.td.source.point(xs, signal, ts, grid) sfs.plot.level(p, grid) """ @@ -129,7 +129,7 @@ def point_image_sources(x0, signal, observation_time, grid, L, max_order, order = 2 # image source order coeffs = .8, .8, .6, .6, .7, .7 # wall reflection coefficients grid = sfs.util.xyz_grid([0, room[0]], [0, room[1]], 0, spacing=0.01) - p = sfs.time.source.point_image_sources( + p = sfs.td.source.point_image_sources( xs, signal, 1.5 * ts, grid, room, order, coeffs) sfs.plot.level(p, grid) diff --git a/sfs/time/wfs.py b/sfs/td/wfs.py similarity index 92% rename from sfs/time/wfs.py rename to sfs/td/wfs.py index a9280a0..276361f 100644 --- a/sfs/time/wfs.py +++ b/sfs/td/wfs.py @@ -36,8 +36,8 @@ grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02) def plot(d, selection, secondary_source, t=0): - p = sfs.time.synthesize(d, selection, array, secondary_source, grid=grid, - observation_time=t) + p = sfs.td.synthesize(d, selection, array, secondary_source, grid=grid, + observation_time=t) sfs.plot.level(p, grid) sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) @@ -76,7 +76,7 @@ def plane_25d(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.time.synthesize()`. + single secondary source. See `sfs.td.synthesize()`. Notes ----- @@ -106,8 +106,8 @@ def plane_25d(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): :context: close-figs delays, weights, selection, secondary_source = \ - sfs.time.wfs.plane_25d(array.x, array.n, npw) - d = sfs.time.wfs.driving_signals(delays, weights, signal) + sfs.td.wfs.plane_25d(array.x, array.n, npw) + d = sfs.td.wfs.driving_signals(delays, weights, signal) plot(d, selection, secondary_source) """ @@ -151,7 +151,7 @@ def point_25d(x0, n0, xs, xref=[0, 0, 0], c=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.time.synthesize()`. + single secondary source. See `sfs.td.synthesize()`. Notes ----- @@ -183,8 +183,8 @@ def point_25d(x0, n0, xs, xref=[0, 0, 0], c=None): :context: close-figs delays, weights, selection, secondary_source = \ - sfs.time.wfs.point_25d(array.x, array.n, xs) - d = sfs.time.wfs.driving_signals(delays, weights, signal) + sfs.td.wfs.point_25d(array.x, array.n, xs) + d = sfs.td.wfs.driving_signals(delays, weights, signal) plot(d, selection, secondary_source, t=ts) """ @@ -234,7 +234,7 @@ def focused_25d(x0, n0, xs, ns, xref=[0, 0, 0], c=None): whether the corresponding secondary source is "active" or not. secondary_source_function : callable A function that can be used to create the sound field of a - single secondary source. See `sfs.time.synthesize()`. + single secondary source. See `sfs.td.synthesize()`. Notes ----- @@ -267,8 +267,8 @@ def focused_25d(x0, n0, xs, ns, xref=[0, 0, 0], c=None): :context: close-figs delays, weights, selection, secondary_source = \ - sfs.time.wfs.focused_25d(array.x, array.n, xf, nf) - d = sfs.time.wfs.driving_signals(delays, weights, signal) + sfs.td.wfs.focused_25d(array.x, array.n, xf, nf) + d = sfs.td.wfs.driving_signals(delays, weights, signal) plot(d, selection, secondary_source, t=tf) """ From 1b879c20710eb1921e250714ce77f7e9a17d8e4b Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 14 Mar 2019 19:26:30 +0100 Subject: [PATCH 064/101] Use Python 3 sequence unpacking --- sfs/util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sfs/util.py b/sfs/util.py index d27311d..5bfab96 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -217,8 +217,7 @@ def as_delayed_signal(arg, **kwargs): """ try: - # In Python 3, this could be: data, samplerate, *time = arg - data, samplerate, time = arg[0], arg[1], arg[2:] + data, samplerate, *time = arg time, = time or [0] except (IndexError, TypeError, ValueError): pass From 7a962e727599cc2effadacc024629205c5c888d2 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Fri, 15 Mar 2019 20:57:14 +0100 Subject: [PATCH 065/101] DOC: Minor updates to the "installation" docs --- doc/installation.rst | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/doc/installation.rst b/doc/installation.rst index f77f2de..f751f90 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -5,17 +5,21 @@ Requirements ------------ Obviously, you'll need Python_. -We normally use Python 3.x, but it *should* also work with Python 2.x. +More specifically, you'll need Python 3. NumPy_ and SciPy_ are needed for the calculations. -If you also want to plot the resulting sound fields, you'll need matplotlib_. +If you want to use the provided functions for plotting sound fields, you'll need +Matplotlib_. +However, since all results are provided as plain NumPy_ arrays, you should also +be able to use any plotting library of your choice to visualize the sound +fields. -Instead of installing all of them separately, you should probably get a Python -distribution that already includes everything, e.g. Anaconda_. +Instead of installing all of the requirements separately, you should probably +get a Python distribution that already includes everything, e.g. Anaconda_. .. _Python: https://www.python.org/ .. _NumPy: http://www.numpy.org/ .. _SciPy: https://www.scipy.org/scipylib/ -.. _matplotlib: https://matplotlib.org/ +.. _Matplotlib: https://matplotlib.org/ .. _Anaconda: https://docs.anaconda.com/anaconda/ Installation @@ -34,4 +38,7 @@ To un-install, use:: python3 -m pip uninstall sfs +If you want to install the latest development version of the SFS Toolbox, have a +look at :doc:`contributing`. + .. _pip: https://pip.pypa.io/en/latest/installing/ From 577c2aa5d43045d2c9a01f609bb0079cbabdabcd Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Fri, 15 Mar 2019 21:05:47 +0100 Subject: [PATCH 066/101] DOC: Mention the example script animations_pulsating_sphere.py --- doc/example-python-scripts.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/example-python-scripts.rst b/doc/example-python-scripts.rst index 650e884..85b0436 100644 --- a/doc/example-python-scripts.rst +++ b/doc/example-python-scripts.rst @@ -7,5 +7,8 @@ Various example scripts are located in the directory ``doc/examples/``, e.g. of the toolbox * :download:`examples/horizontal_plane_arrays.py`: Computes the sound fields for various techniques, virtual sources and loudspeaker array configurations +* :download:`examples/animations_pulsating_sphere.py`: Creates animations of a + pulsating sphere, see also `the corresponding Jupyter notebook + `__ * :download:`examples/soundfigures.py`: Illustrates the synthesis of sound figures with Wave Field Synthesis From e9b2698d948edcf615d998ecd7b07c37b6901a73 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Fri, 15 Mar 2019 21:25:31 +0100 Subject: [PATCH 067/101] DOC: Create links when np.ndarray is the return type --- sfs/tapering.py | 4 ++-- sfs/util.py | 21 ++++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/sfs/tapering.py b/sfs/tapering.py index f5d6d6f..1541eef 100644 --- a/sfs/tapering.py +++ b/sfs/tapering.py @@ -67,7 +67,7 @@ def tukey(active, alpha): Returns ------- - (len(active),) numpy.ndarray + (len(active),) `numpy.ndarray` Tapering weights. Examples @@ -123,7 +123,7 @@ def kaiser(active, beta): Returns ------- - (len(active),) numpy.ndarray + (len(active),) `numpy.ndarray` Tapering weights. Examples diff --git a/sfs/util.py b/sfs/util.py index 5bfab96..22a4185 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -21,7 +21,7 @@ def rotation_matrix(n1, n2): Returns ------- - (3, 3) numpy.ndarray + (3, 3) `numpy.ndarray` Rotation matrix. """ @@ -78,18 +78,17 @@ def sph2cart(alpha, beta, r): Returns ------- - x : float or array_like + x : float or `numpy.ndarray` x-component of Cartesian coordinates - y : float or array_like + y : float or `numpy.ndarray` y-component of Cartesian coordinates - z : float or array_like + z : float or `numpy.ndarray` z-component of Cartesian coordinates """ x = r * np.cos(alpha) * np.sin(beta) y = r * np.sin(alpha) * np.sin(beta) z = r * np.cos(beta) - return x, y, z @@ -115,11 +114,11 @@ def cart2sph(x, y, z): Returns ------- - alpha : float or array_like + alpha : float or `numpy.ndarray` Azimuth angle in radiants - beta : float or array_like + beta : float or `numpy.ndarray` Colatitude angle in radiants (with 0 denoting North pole) - r : float or array_like + r : float or `numpy.ndarray` Radius """ @@ -252,7 +251,7 @@ def strict_arange(start, stop, step=1, endpoint=False, dtype=None, **kwargs): Returns ------- - numpy.ndarray + `numpy.ndarray` Array of evenly spaced values. See :func:`numpy.arange`. """ @@ -508,9 +507,9 @@ def image_sources_for_box(x, L, N, prune=True): Returns ------- - xs : (M, D) array_like + xs : (M, D) `numpy.ndarray` original & image source locations. - wall_count : (M, 2D) array_like + wall_count : (M, 2D) `numpy.ndarray` number of reflections at individual walls for each source. """ From 4f83604344d792ea436750f713d47926456dbb2b Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Fri, 15 Mar 2019 11:30:58 +0100 Subject: [PATCH 068/101] Split "plot" module into "plot2d" and "plot3d" See #26. --- doc/examples/animations_pulsating_sphere.py | 6 +-- doc/examples/horizontal_plane_arrays.py | 8 ++-- doc/examples/mirror-image-source-model.ipynb | 6 +-- doc/examples/modal-room-acoustics.ipynb | 2 +- doc/examples/plot_particle_density.py | 2 +- doc/examples/sound_field_synthesis.py | 6 +-- doc/examples/soundfigures.py | 8 ++-- doc/examples/time_domain.py | 18 +++---- doc/examples/time_domain_nfchoa.py | 12 ++--- sfs/__init__.py | 9 +++- sfs/array.py | 26 +++++------ sfs/fd/nfchoa.py | 4 +- sfs/fd/sdm.py | 4 +- sfs/fd/source.py | 34 +++++++------- sfs/fd/wfs.py | 4 +- sfs/{plot.py => plot2d.py} | 49 +++++++------------- sfs/plot3d.py | 15 ++++++ sfs/td/nfchoa.py | 4 +- sfs/td/source.py | 4 +- sfs/td/wfs.py | 4 +- 20 files changed, 116 insertions(+), 109 deletions(-) rename sfs/{plot.py => plot2d.py} (90%) create mode 100644 sfs/plot3d.py diff --git a/doc/examples/animations_pulsating_sphere.py b/doc/examples/animations_pulsating_sphere.py index 2257c48..9fb741d 100644 --- a/doc/examples/animations_pulsating_sphere.py +++ b/doc/examples/animations_pulsating_sphere.py @@ -15,7 +15,7 @@ def particle_displacement(omega, center, radius, amplitude, grid, frames, fig, ax = plt.subplots(figsize=figsize) ax.axis([grid[0].min(), grid[0].max(), grid[1].min(), grid[1].max()]) - scat = sfs.plot.particles(grid + displacement, **kwargs) + scat = sfs.plot2d.particles(grid + displacement, **kwargs) def update_frame_displacement(i): position = (grid + displacement * phasor**i).apply(np.real) @@ -38,7 +38,7 @@ def particle_velocity(omega, center, radius, amplitude, grid, frames, fig, ax = plt.subplots(figsize=figsize) ax.axis([grid[0].min(), grid[0].max(), grid[1].min(), grid[1].max()]) - quiv = sfs.plot.vectors( + quiv = sfs.plot2d.vectors( velocity, grid, clim=[-omega * amplitude, omega * amplitude], **kwargs) @@ -59,7 +59,7 @@ def sound_pressure(omega, center, radius, amplitude, grid, frames, phasor = np.exp(1j * 2 * np.pi / frames) fig, ax = plt.subplots(figsize=figsize) - im = sfs.plot.soundfield(np.real(pressure), grid, **kwargs) + im = sfs.plot2d.amplitude(np.real(pressure), grid, **kwargs) ax.axis([grid[0].min(), grid[0].max(), grid[1].min(), grid[1].max()]) def update_frame_pressure(i): diff --git a/doc/examples/horizontal_plane_arrays.py b/doc/examples/horizontal_plane_arrays.py index 3cb677a..8fa9580 100644 --- a/doc/examples/horizontal_plane_arrays.py +++ b/doc/examples/horizontal_plane_arrays.py @@ -34,10 +34,10 @@ def compute_and_plot_soundfield(title): plt.figure(figsize=(15, 15)) plt.cla() - sfs.plot.soundfield(p, grid, xnorm) - sfs.plot.loudspeaker_2d(array.x, array.n, twin) - sfs.plot.virtualsource_2d(xs) - sfs.plot.virtualsource_2d([0, 0], npw, type='plane') + sfs.plot2d.amplitude(p, grid, xnorm) + sfs.plot2d.loudspeakers(array.x, array.n, twin) + sfs.plot2d.virtualsource(xs) + sfs.plot2d.virtualsource([0, 0], npw, type='plane') plt.title(title) plt.grid() plt.savefig(title + '.png') diff --git a/doc/examples/mirror-image-source-model.ipynb b/doc/examples/mirror-image-source-model.ipynb index 7b9afa0..b99c133 100644 --- a/doc/examples/mirror-image-source-model.ipynb +++ b/doc/examples/mirror-image-source-model.ipynb @@ -103,7 +103,7 @@ "metadata": {}, "outputs": [], "source": [ - "sfs.plot.soundfield(P, grid, xnorm=[L[0]/2, L[1]/2, L[2]/2]);" + "sfs.plot2d.amplitude(P, grid, xnorm=[L[0]/2, L[1]/2, L[2]/2]);" ] }, { @@ -140,8 +140,8 @@ "metadata": {}, "outputs": [], "source": [ - "sfs.plot.level(p, grid)\n", - "sfs.plot.virtualsource_2d(x0)" + "sfs.plot2d.level(p, grid)\n", + "sfs.plot2d.virtualsource(x0)" ] } ], diff --git a/doc/examples/modal-room-acoustics.ipynb b/doc/examples/modal-room-acoustics.ipynb index ef7adf9..26164bd 100644 --- a/doc/examples/modal-room-acoustics.ipynb +++ b/doc/examples/modal-room-acoustics.ipynb @@ -114,7 +114,7 @@ "metadata": {}, "outputs": [], "source": [ - "sfs.plot.soundfield(p, grid);" + "sfs.plot2d.amplitude(p, grid);" ] }, { diff --git a/doc/examples/plot_particle_density.py b/doc/examples/plot_particle_density.py index 6f93f74..9de8845 100644 --- a/doc/examples/plot_particle_density.py +++ b/doc/examples/plot_particle_density.py @@ -23,7 +23,7 @@ def plot_particle_displacement(title): # plot displacement plt.figure(figsize=(15, 15)) plt.cla() - sfs.plot.particles(X, facecolor='black', s=3, trim=[-3, 3, -3, 3]) + sfs.plot2d.particles(X, facecolor='black', s=3, trim=[-3, 3, -3, 3]) plt.axis('off') plt.title(title) plt.grid() diff --git a/doc/examples/sound_field_synthesis.py b/doc/examples/sound_field_synthesis.py index 55d2cf8..8c5f723 100644 --- a/doc/examples/sound_field_synthesis.py +++ b/doc/examples/sound_field_synthesis.py @@ -73,11 +73,11 @@ # === plot synthesized sound field === plt.figure(figsize=(10, 10)) -sfs.plot.soundfield(p, grid, [0, 0, 0]) -sfs.plot.loudspeaker_2d(array.x, array.n, twin) +sfs.plot2d.amplitude(p, grid, [0, 0, 0]) +sfs.plot2d.loudspeakers(array.x, array.n, twin) plt.grid() plt.savefig('soundfield.png') -#sfs.plot.loudspeaker_3d(array.x, array.n, twin) +#sfs.plot3d.secondary_sources(array.x, array.n, twin) #plt.savefig('loudspeakers.png') diff --git a/doc/examples/soundfigures.py b/doc/examples/soundfigures.py index 7260db6..6bb763b 100644 --- a/doc/examples/soundfigures.py +++ b/doc/examples/soundfigures.py @@ -37,14 +37,14 @@ # plot and save synthesized sound field plt.figure(figsize=(10, 10)) -sfs.plot.soundfield(p, grid, xnorm=[0, -2.2, 0], cmap='BrBG', colorbar=False, - vmin=-1, vmax=1) +sfs.plot2d.amplitude(p, grid, xnorm=[0, -2.2, 0], cmap='BrBG', colorbar=False, + vmin=-1, vmax=1) plt.title('Synthesized Sound Field') plt.savefig('soundfigure.png') # plot and save level of synthesized sound field plt.figure(figsize=(12.5, 12.5)) -im = sfs.plot.level(p, grid, xnorm=[0, -2.2, 0], vmin=-50, vmax=0, - colorbar_kwargs=dict(label='dB')) +im = sfs.plot2d.level(p, grid, xnorm=[0, -2.2, 0], vmin=-50, vmax=0, + colorbar_kwargs=dict(label='dB')) plt.title('Level of Synthesized Sound Field') plt.savefig('soundfigure_level.png') diff --git a/doc/examples/time_domain.py b/doc/examples/time_domain.py index 79d965b..858e2a7 100644 --- a/doc/examples/time_domain.py +++ b/doc/examples/time_domain.py @@ -36,10 +36,10 @@ p = p * 100 # scale absolute amplitude plt.figure(figsize=(10, 10)) -sfs.plot.level(p, grid, cmap=my_cmap) -sfs.plot.loudspeaker_2d(array.x, array.n, twin) +sfs.plot2d.level(p, grid, cmap=my_cmap) +sfs.plot2d.loudspeakers(array.x, array.n, twin) plt.grid() -sfs.plot.virtualsource_2d(xs) +sfs.plot2d.virtualsource(xs) plt.title('impulse_ps_wfs_25d') plt.savefig('impulse_ps_wfs_25d.png') @@ -59,10 +59,10 @@ secondary_source, observation_time=t, grid=grid) plt.figure(figsize=(10, 10)) -sfs.plot.level(p, grid, cmap=my_cmap) -sfs.plot.loudspeaker_2d(array.x, array.n, twin) +sfs.plot2d.level(p, grid, cmap=my_cmap) +sfs.plot2d.loudspeakers(array.x, array.n, twin) plt.grid() -sfs.plot.virtualsource_2d([0, 0], npw, type='plane') +sfs.plot2d.virtualsource([0, 0], npw, type='plane') plt.title('impulse_pw_wfs_25d') plt.savefig('impulse_pw_wfs_25d.png') @@ -82,9 +82,9 @@ p = p * 100 # scale absolute amplitude plt.figure(figsize=(10, 10)) -sfs.plot.level(p, grid, cmap=my_cmap) -sfs.plot.loudspeaker_2d(array.x, array.n, twin) +sfs.plot2d.level(p, grid, cmap=my_cmap) +sfs.plot2d.loudspeakers(array.x, array.n, twin) plt.grid() -sfs.plot.virtualsource_2d(xs) +sfs.plot2d.virtualsource(xs) plt.title('impulse_fs_wfs_25d') plt.savefig('impulse_fs_wfs_25d.png') diff --git a/doc/examples/time_domain_nfchoa.py b/doc/examples/time_domain_nfchoa.py index c884832..929c1ef 100644 --- a/doc/examples/time_domain_nfchoa.py +++ b/doc/examples/time_domain_nfchoa.py @@ -27,9 +27,9 @@ observation_time=t, grid=grid) plt.figure() -sfs.plot.level(p, grid) -sfs.plot.loudspeaker_2d(array.x, array.n) -sfs.plot.virtualsource_2d([0, 0], ns=npw, type='plane') +sfs.plot2d.level(p, grid) +sfs.plot2d.loudspeakers(array.x, array.n) +sfs.plot2d.virtualsource([0, 0], ns=npw, type='plane') plt.savefig('impulse_pw_nfchoa_25d.png') # Point source @@ -44,7 +44,7 @@ observation_time=t, grid=grid) plt.figure() -sfs.plot.level(p, grid) -sfs.plot.loudspeaker_2d(array.x, array.n) -sfs.plot.virtualsource_2d(xs, type='point') +sfs.plot2d.level(p, grid) +sfs.plot2d.loudspeakers(array.x, array.n) +sfs.plot2d.virtualsource(xs, type='point') plt.savefig('impulse_ps_nfchoa_25d.png') diff --git a/sfs/__init__.py b/sfs/__init__.py index 5ea1dcf..a1c356f 100644 --- a/sfs/__init__.py +++ b/sfs/__init__.py @@ -11,7 +11,8 @@ td array tapering - plot + plot2d + plot3d util """ @@ -59,7 +60,11 @@ def reset(self): from . import array from . import util try: - from . import plot + from . import plot2d +except ImportError: + pass +try: + from . import plot3d except ImportError: pass diff --git a/sfs/array.py b/sfs/array.py index d48b584..f3ba7d6 100644 --- a/sfs/array.py +++ b/sfs/array.py @@ -110,7 +110,7 @@ def linear(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): :context: close-figs x0, n0, a0 = sfs.array.linear(16, 0.2, orientation=[0, -1, 0]) - sfs.plot.loudspeaker_2d(x0, n0, a0) + sfs.plot2d.loudspeakers(x0, n0, a0) plt.axis('equal') plt.xlabel('x / m') plt.ylabel('y / m') @@ -141,7 +141,7 @@ def linear_diff(distances, center=[0, 0, 0], orientation=[1, 0, 0]): x0, n0, a0 = sfs.array.linear_diff(4 * [0.3] + 6 * [0.15] + 4 * [0.3], orientation=[0, -1, 0]) - sfs.plot.loudspeaker_2d(x0, n0, a0) + sfs.plot2d.loudspeakers(x0, n0, a0) plt.axis('equal') plt.xlabel('x / m') plt.ylabel('y / m') @@ -182,7 +182,7 @@ def linear_random(N, min_spacing, max_spacing, center=[0, 0, 0], N=12, min_spacing=0.15, max_spacing=0.4, orientation=[0, -1, 0]) - sfs.plot.loudspeaker_2d(x0, n0, a0) + sfs.plot2d.loudspeakers(x0, n0, a0) plt.axis('equal') plt.xlabel('x / m') plt.ylabel('y / m') @@ -216,7 +216,7 @@ def circular(N, R, center=[0, 0, 0]): :context: close-figs x0, n0, a0 = sfs.array.circular(16, 1) - sfs.plot.loudspeaker_2d(x0, n0, a0, size=0.2, show_numbers=True) + sfs.plot2d.loudspeakers(x0, n0, a0, size=0.2, show_numbers=True) plt.axis('equal') plt.xlabel('x / m') plt.ylabel('y / m') @@ -262,7 +262,7 @@ def rectangular(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): :context: close-figs x0, n0, a0 = sfs.array.rectangular((4, 8), 0.2) - sfs.plot.loudspeaker_2d(x0, n0, a0, show_numbers=True) + sfs.plot2d.loudspeakers(x0, n0, a0, show_numbers=True) plt.axis('equal') plt.xlabel('x / m') plt.ylabel('y / m') @@ -309,7 +309,7 @@ def rounded_edge(Nxy, Nr, dx, center=[0, 0, 0], orientation=[1, 0, 0]): :context: close-figs x0, n0, a0 = sfs.array.rounded_edge(8, 5, 0.2) - sfs.plot.loudspeaker_2d(x0, n0, a0) + sfs.plot2d.loudspeakers(x0, n0, a0) plt.axis('equal') plt.xlabel('x / m') plt.ylabel('y / m') @@ -380,7 +380,7 @@ def edge(Nxy, dx, center=[0, 0, 0], orientation=[1, 0, 0]): :context: close-figs x0, n0, a0 = sfs.array.edge(8, 0.2) - sfs.plot.loudspeaker_2d(x0, n0, a0) + sfs.plot2d.loudspeakers(x0, n0, a0) plt.axis('equal') plt.xlabel('x / m') plt.ylabel('y / m') @@ -441,7 +441,7 @@ def planar(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): 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 + sfs.plot2d.loudspeakers(x0, n0, a0) # plot the last ssd in 2D plt.axis('equal') plt.xlabel('x / m') plt.ylabel('y / m') @@ -488,7 +488,7 @@ def cube(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): 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) + sfs.plot2d.loudspeakers(x0, n0, a0) plt.axis('equal') plt.xlabel('x / m') plt.ylabel('y / m') @@ -547,7 +547,7 @@ def sphere_load(file, radius, center=[0, 0, 0]): '../data/arrays/example_array_6LS_3D.txt', radius=2, center=[0, 0, 0]) - sfs.plot.loudspeaker_2d(x0, n0, a0, size=0.25) + sfs.plot2d.loudspeakers(x0, n0, a0, size=0.25) plt.axis('equal') plt.xlabel('x / m') plt.ylabel('y / m') @@ -590,7 +590,7 @@ def load(file, center=[0, 0, 0], orientation=[1, 0, 0]): :context: close-figs x0, n0, a0 = sfs.array.load('../data/arrays/example_array_4LS_2D.csv') - sfs.plot.loudspeaker_2d(x0, n0, a0) + sfs.plot2d.loudspeakers(x0, n0, a0) plt.axis('equal') plt.xlabel('x / m') plt.ylabel('y / m') @@ -600,7 +600,7 @@ def load(file, center=[0, 0, 0], orientation=[1, 0, 0]): x0, n0, a0 = sfs.array.load( '../data/arrays/wfs_university_rostock_2018.csv') - sfs.plot.loudspeaker_2d(x0, n0, a0) + sfs.plot2d.loudspeakers(x0, n0, a0) plt.axis('equal') plt.xlabel('x / m') plt.ylabel('y / m') @@ -695,7 +695,7 @@ def concatenate(*arrays): 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) + sfs.plot2d.loudspeakers(x0, n0, a0) plt.axis('equal') plt.xlabel('x / m') plt.ylabel('y / m') diff --git a/sfs/fd/nfchoa.py b/sfs/fd/nfchoa.py index 2a8312b..55322a8 100644 --- a/sfs/fd/nfchoa.py +++ b/sfs/fd/nfchoa.py @@ -24,8 +24,8 @@ def plot(d, selection, secondary_source): p = sfs.fd.synthesize(d, selection, array, secondary_source, grid=grid) - sfs.plot.soundfield(p, grid) - sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) + sfs.plot2d.amplitude(p, grid) + sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15) """ diff --git a/sfs/fd/sdm.py b/sfs/fd/sdm.py index b3af25f..a14d7ed 100644 --- a/sfs/fd/sdm.py +++ b/sfs/fd/sdm.py @@ -23,8 +23,8 @@ def plot(d, selection, secondary_source): p = sfs.fd.synthesize(d, selection, array, secondary_source, grid=grid) - sfs.plot.soundfield(p, grid) - sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) + sfs.plot2d.amplitude(p, grid) + sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15) """ diff --git a/sfs/fd/source.py b/sfs/fd/source.py index c7581a9..568317f 100644 --- a/sfs/fd/source.py +++ b/sfs/fd/source.py @@ -64,7 +64,7 @@ def point(omega, x0, grid, c=None): :context: close-figs p = sfs.fd.source.point(omega, x0, grid) - sfs.plot.soundfield(p, grid) + sfs.plot2d.amplitude(p, grid) plt.title("Point Source at {} m".format(x0)) Normalization ... @@ -72,8 +72,8 @@ def point(omega, x0, grid, c=None): .. plot:: :context: close-figs - sfs.plot.soundfield(p * normalization_point, grid, - colorbar_kwargs=dict(label="p / Pa")) + sfs.plot2d.amplitude(p * normalization_point, grid, + colorbar_kwargs=dict(label="p / Pa")) plt.title("Point Source at {} m (normalized)".format(x0)) """ @@ -115,8 +115,8 @@ def point_velocity(omega, x0, grid, c=None, rho0=None): :context: close-figs v = sfs.fd.source.point_velocity(omega, x0, vgrid) - sfs.plot.soundfield(p * normalization_point, grid) - sfs.plot.vectors(v * normalization_point, vgrid) + sfs.plot2d.amplitude(p * normalization_point, grid) + sfs.plot2d.vectors(v * normalization_point, vgrid) plt.title("Sound Pressure and Particle Velocity") """ @@ -206,7 +206,7 @@ def point_dipole(omega, x0, n0, grid, c=None): n0 = 0, 1, 0 p = sfs.fd.source.point_dipole(omega, x0, n0, grid) - sfs.plot.soundfield(p, grid) + sfs.plot2d.amplitude(p, grid) plt.title("Dipole Point Source at {} m".format(x0)) """ @@ -417,7 +417,7 @@ def line(omega, x0, grid, c=None): :context: close-figs p = sfs.fd.source.line(omega, x0, grid) - sfs.plot.soundfield(p, grid) + sfs.plot2d.amplitude(p, grid) plt.title("Line Source at {} m".format(x0[:2])) Normalization ... @@ -425,8 +425,8 @@ def line(omega, x0, grid, c=None): .. plot:: :context: close-figs - sfs.plot.soundfield(p * normalization_line, grid, - colorbar_kwargs=dict(label="p / Pa")) + sfs.plot2d.amplitude(p * normalization_line, grid, + colorbar_kwargs=dict(label="p / Pa")) plt.title("Line Source at {} m (normalized)".format(x0[:2])) """ @@ -455,8 +455,8 @@ def line_velocity(omega, x0, grid, c=None, rho0=None): :context: close-figs v = sfs.fd.source.line_velocity(omega, x0, vgrid) - sfs.plot.soundfield(p * normalization_line, grid) - sfs.plot.vectors(v * normalization_line, vgrid) + sfs.plot2d.amplitude(p * normalization_line, grid) + sfs.plot2d.vectors(v * normalization_line, vgrid) plt.title("Sound Pressure and Particle Velocity") """ @@ -606,7 +606,7 @@ def plane(omega, x0, n0, grid, c=None): direction = 45 # degree n0 = sfs.util.direction_vector(np.radians(direction)) p = sfs.fd.source.plane(omega, x0, n0, grid) - sfs.plot.soundfield(p, grid, colorbar_kwargs=dict(label="p / Pa")) + sfs.plot2d.amplitude(p, grid, colorbar_kwargs=dict(label="p / Pa")) plt.title("Plane wave with direction {} degree".format(direction)) """ @@ -655,8 +655,8 @@ def plane_velocity(omega, x0, n0, grid, c=None, rho0=None): :context: close-figs v = sfs.fd.source.plane_velocity(omega, x0, n0, vgrid) - sfs.plot.soundfield(p, grid) - sfs.plot.vectors(v, vgrid) + sfs.plot2d.amplitude(p, grid) + sfs.plot2d.vectors(v, vgrid) plt.title("Sound Pressure and Particle Velocity") """ @@ -745,7 +745,7 @@ def pulsating_sphere(omega, center, radius, amplitude, grid, inside=False, radius = 0.25 amplitude = 1 / (radius * omega * sfs.default.rho0 * sfs.default.c) p = sfs.fd.source.pulsating_sphere(omega, x0, radius, amplitude, grid) - sfs.plot.soundfield(p, grid) + sfs.plot2d.amplitude(p, grid) plt.title("Sound Pressure of a Pulsating Sphere") """ @@ -797,8 +797,8 @@ def pulsating_sphere_velocity(omega, center, radius, amplitude, grid, c=None): :context: close-figs v = sfs.fd.source.pulsating_sphere_velocity(omega, x0, radius, amplitude, vgrid) - sfs.plot.soundfield(p, grid) - sfs.plot.vectors(v, vgrid) + sfs.plot2d.amplitude(p, grid) + sfs.plot2d.vectors(v, vgrid) plt.title("Sound Pressure and Particle Velocity of a Pulsating Sphere") """ diff --git a/sfs/fd/wfs.py b/sfs/fd/wfs.py index 32e805d..66a5fea 100644 --- a/sfs/fd/wfs.py +++ b/sfs/fd/wfs.py @@ -27,8 +27,8 @@ def plot(d, selection, secondary_source): p = sfs.fd.synthesize(d, selection, array, secondary_source, grid=grid) - sfs.plot.soundfield(p, grid) - sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) + sfs.plot2d.amplitude(p, grid) + sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15) """ diff --git a/sfs/plot.py b/sfs/plot2d.py similarity index 90% rename from sfs/plot.py rename to sfs/plot2d.py index a8b4d59..fd76a36 100644 --- a/sfs/plot.py +++ b/sfs/plot2d.py @@ -1,11 +1,10 @@ -"""Plot sound fields etc.""" +"""2D plots of sound fields etc.""" import matplotlib.pyplot as plt from matplotlib import __version__ as matplotlib_version from matplotlib.patches import PathPatch from matplotlib.path import Path from matplotlib.collections import PatchCollection from mpl_toolkits import axes_grid1 -from mpl_toolkits.mplot3d import Axes3D import numpy as np from . import util from . import default @@ -42,7 +41,7 @@ def _register_cmap_transparent(name, color): _register_cmap_transparent('blacktransparent', 'black') -def virtualsource_2d(xs, ns=None, type='point', ax=None): +def virtualsource(xs, ns=None, type='point', ax=None): """Draw position/orientation of virtual source.""" xs = np.asarray(xs) ns = np.asarray(ns) @@ -62,7 +61,7 @@ def virtualsource_2d(xs, ns=None, type='point', ax=None): head_length=0.1, fc='k', ec='k') -def reference_2d(xref, size=0.1, ax=None): +def reference(xref, size=0.1, ax=None): """Draw reference/normalization point.""" xref = np.asarray(xref) if ax is None: @@ -72,7 +71,7 @@ def reference_2d(xref, size=0.1, ax=None): ax.plot((xref[0]-size, xref[0]+size), (xref[1]+size, xref[1]-size), 'k-') -def secondarysource_2d(x0, n0, grid=None): +def secondary_sources(x0, n0, grid=None): """Simple plot of secondary source locations.""" x0 = np.asarray(x0) n0 = np.asarray(n0) @@ -80,7 +79,7 @@ def secondarysource_2d(x0, n0, grid=None): # plot only secondary sources inside simulated area if grid is not None: - x0, n0 = _visible_secondarysources_2d(x0, n0, grid) + x0, n0 = _visible_secondarysources(x0, n0, grid) # plot symbols for x00 in x0: @@ -88,8 +87,8 @@ def secondarysource_2d(x0, n0, grid=None): ax.add_artist(ss) -def loudspeaker_2d(x0, n0, a0=0.5, size=0.08, show_numbers=False, grid=None, - ax=None): +def loudspeakers(x0, n0, a0=0.5, size=0.08, show_numbers=False, grid=None, + ax=None): """Draw loudspeaker symbols at given locations and angles. Parameters @@ -117,7 +116,7 @@ def loudspeaker_2d(x0, n0, a0=0.5, size=0.08, show_numbers=False, grid=None, # plot only secondary sources inside simulated area if grid is not None: - x0, n0 = _visible_secondarysources_2d(x0, n0, grid) + x0, n0 = _visible_secondarysources(x0, n0, grid) # normalized coordinates of loudspeaker symbol (see IEC 60617-9) codes, coordinates = zip(*( @@ -155,7 +154,7 @@ def loudspeaker_2d(x0, n0, a0=0.5, size=0.08, show_numbers=False, grid=None, verticalalignment='center', clip_on=True) -def _visible_secondarysources_2d(x0, n0, grid): +def _visible_secondarysources(x0, n0, grid): """Determine secondary sources which lie within *grid*.""" x, y = util.as_xyz_components(grid[:2]) idx = np.where((x0[:, 0] > x.min()) & (x0[:, 0] < x.max()) & @@ -165,22 +164,10 @@ def _visible_secondarysources_2d(x0, n0, grid): return x0[idx, :], n0[idx, :] -def loudspeaker_3d(x0, n0, a0=None, w=0.08, h=0.08): - """Plot positions and normals of a 3D secondary source distribution.""" - fig = plt.figure(figsize=(15, 15)) - ax = fig.add_subplot(111, projection='3d') - ax.quiver(x0[:, 0], x0[:, 1], x0[:, 2], n0[:, 0], - n0[:, 1], n0[:, 2], length=0.1) - plt.xlabel('x (m)') - plt.ylabel('y (m)') - plt.title('Secondary Sources') - fig.show() - - -def soundfield(p, grid, xnorm=None, cmap='coolwarm_clip', vmin=-2.0, vmax=2.0, - xlabel=None, ylabel=None, colorbar=True, colorbar_kwargs={}, - ax=None, **kwargs): - """Two-dimensional plot of sound field. +def amplitude(p, grid, xnorm=None, cmap='coolwarm_clip', vmin=-2.0, vmax=2.0, + xlabel=None, ylabel=None, colorbar=True, colorbar_kwargs={}, + ax=None, **kwargs): + """Two-dimensional plot of sound field (real part). Parameters ---------- @@ -240,7 +227,7 @@ def soundfield(p, grid, xnorm=None, cmap='coolwarm_clip', vmin=-2.0, vmax=2.0, See Also -------- - sfs.plot.level + sfs.plot2d.level """ p = np.asarray(p) @@ -311,7 +298,7 @@ def level(p, grid, xnorm=None, power=False, cmap=None, vmax=3, vmin=-50, **kwargs): """Two-dimensional plot of level (dB) of sound field. - Takes the same parameters as `sfs.plot.soundfield()`. + Takes the same parameters as `sfs.plot2d.amplitude()`. Other Parameters ---------------- @@ -323,8 +310,8 @@ def level(p, grid, xnorm=None, power=False, cmap=None, vmax=3, vmin=-50, if xnorm is not None: p = util.normalize(p, grid, xnorm) L = util.db(p, power=power) - return soundfield(L, grid=grid, xnorm=None, cmap=cmap, - vmax=vmax, vmin=vmin, **kwargs) + return amplitude(L, grid=grid, xnorm=None, cmap=cmap, + vmax=vmax, vmin=vmin, **kwargs) def particles(x, trim=None, ax=None, xlabel='x (m)', ylabel='y (m)', @@ -403,7 +390,7 @@ def add_colorbar(im, aspect=20, pad=0.5, **kwargs): Parameters ---------- im : ScalarMappable - The output of `sfs.plot.soundfield()`, `sfs.plot.level()` or any + The output of `sfs.plot2d.amplitude()`, `sfs.plot2d.level()` or any other `matplotlib.cm.ScalarMappable`. aspect : float, optional Aspect ratio of the colorbar. Strictly speaking, since the diff --git a/sfs/plot3d.py b/sfs/plot3d.py new file mode 100644 index 0000000..9cf4cb0 --- /dev/null +++ b/sfs/plot3d.py @@ -0,0 +1,15 @@ +"""3D plots of sound fields etc.""" +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D + + +def secondary_sources(x0, n0, a0=None, w=0.08, h=0.08): + """Plot positions and normals of a 3D secondary source distribution.""" + fig = plt.figure(figsize=(15, 15)) + ax = fig.add_subplot(111, projection='3d') + q = ax.quiver(x0[:, 0], x0[:, 1], x0[:, 2], n0[:, 0], + n0[:, 1], n0[:, 2], length=0.1) + plt.xlabel('x (m)') + plt.ylabel('y (m)') + plt.title('Secondary Sources') + return q diff --git a/sfs/td/nfchoa.py b/sfs/td/nfchoa.py index 74981b0..2e65450 100644 --- a/sfs/td/nfchoa.py +++ b/sfs/td/nfchoa.py @@ -32,8 +32,8 @@ def plot(d, selection, secondary_source, t=0): p = sfs.td.synthesize(d, selection, array, secondary_source, grid=grid, observation_time=t) - sfs.plot.level(p, grid) - sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) + sfs.plot2d.level(p, grid) + sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15) """ import numpy as np diff --git a/sfs/td/source.py b/sfs/td/source.py index 7d915c1..bf9e9aa 100644 --- a/sfs/td/source.py +++ b/sfs/td/source.py @@ -69,7 +69,7 @@ def point(xs, signal, observation_time, grid, c=None): :context: close-figs p = sfs.td.source.point(xs, signal, ts, grid) - sfs.plot.level(p, grid) + sfs.plot2d.level(p, grid) """ xs = util.asarray_1d(xs) @@ -131,7 +131,7 @@ def point_image_sources(x0, signal, observation_time, grid, L, max_order, grid = sfs.util.xyz_grid([0, room[0]], [0, room[1]], 0, spacing=0.01) p = sfs.td.source.point_image_sources( xs, signal, 1.5 * ts, grid, room, order, coeffs) - sfs.plot.level(p, grid) + sfs.plot2d.level(p, grid) """ if coeffs is None: diff --git a/sfs/td/wfs.py b/sfs/td/wfs.py index 276361f..03fa985 100644 --- a/sfs/td/wfs.py +++ b/sfs/td/wfs.py @@ -38,8 +38,8 @@ def plot(d, selection, secondary_source, t=0): p = sfs.td.synthesize(d, selection, array, secondary_source, grid=grid, observation_time=t) - sfs.plot.level(p, grid) - sfs.plot.loudspeaker_2d(array.x, array.n, selection * array.a, size=0.15) + sfs.plot2d.level(p, grid) + sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15) """ import numpy as np From c420d98d7b576eed9bfb2f6f7f741d40adfb13d1 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 14 Mar 2019 18:13:09 +0100 Subject: [PATCH 069/101] Keyword-only arguments everywhere --- doc/examples/horizontal_plane_arrays.py | 4 +- doc/examples/mirror-image-source-model.ipynb | 2 +- doc/examples/sound_field_synthesis.py | 8 +-- doc/examples/time_domain.py | 6 +- sfs/array.py | 63 ++++++++++++-------- sfs/fd/__init__.py | 4 +- sfs/fd/esa.py | 16 ++--- sfs/fd/nfchoa.py | 6 +- sfs/fd/sdm.py | 12 ++-- sfs/fd/source.py | 35 +++++------ sfs/fd/wfs.py | 18 +++--- sfs/plot2d.py | 24 ++++---- sfs/plot3d.py | 2 +- sfs/tapering.py | 28 ++++----- sfs/util.py | 9 +-- 15 files changed, 125 insertions(+), 112 deletions(-) diff --git a/doc/examples/horizontal_plane_arrays.py b/doc/examples/horizontal_plane_arrays.py index 8fa9580..daba525 100644 --- a/doc/examples/horizontal_plane_arrays.py +++ b/doc/examples/horizontal_plane_arrays.py @@ -29,12 +29,12 @@ def compute_and_plot_soundfield(title): """Compute and plot synthesized sound field.""" print('Computing', title) - twin = tapering(selection, talpha) + twin = tapering(selection, alpha=talpha) p = sfs.fd.synthesize(d, twin, array, secondary_source, grid=grid) plt.figure(figsize=(15, 15)) plt.cla() - sfs.plot2d.amplitude(p, grid, xnorm) + sfs.plot2d.amplitude(p, grid, xnorm=xnorm) sfs.plot2d.loudspeakers(array.x, array.n, twin) sfs.plot2d.virtualsource(xs) sfs.plot2d.virtualsource([0, 0], npw, type='plane') diff --git a/doc/examples/mirror-image-source-model.ipynb b/doc/examples/mirror-image-source-model.ipynb index b99c133..c436dd2 100644 --- a/doc/examples/mirror-image-source-model.ipynb +++ b/doc/examples/mirror-image-source-model.ipynb @@ -94,7 +94,7 @@ "source": [ "grid = sfs.util.xyz_grid([0, L[0]], [0, L[1]], 1.5, spacing=0.02)\n", "P = sfs.fd.source.point_image_sources(omega, x0, grid, L,\n", - " max_order, coeffs=coeffs)" + " max_order=max_order, coeffs=coeffs)" ] }, { diff --git a/doc/examples/sound_field_synthesis.py b/doc/examples/sound_field_synthesis.py index 8c5f723..dc8125d 100644 --- a/doc/examples/sound_field_synthesis.py +++ b/doc/examples/sound_field_synthesis.py @@ -49,7 +49,7 @@ #d, selection, secondary_source = sfs.fd.wfs.line_2d(omega, array.x, array.n, xs) #d, selection, secondary_source = sfs.fd.wfs.plane_2d(omega, array.x, array.n, npw) -d, selection, secondary_source = sfs.fd.wfs.plane_25d(omega, array.x, array.n, npw, xref) +d, selection, secondary_source = sfs.fd.wfs.plane_25d(omega, array.x, array.n, npw, xref=xref) #d, selection, secondary_source = sfs.fd.wfs.plane_3d(omega, array.x, array.n, npw) #d, selection, secondary_source = sfs.fd.wfs.point_2d(omega, array.x, array.n, xs) @@ -64,8 +64,8 @@ # === compute tapering window === #twin = sfs.tapering.none(selection) -#twin = sfs.tapering.kaiser(selection, 8.6) -twin = sfs.tapering.tukey(selection, 0.3) +#twin = sfs.tapering.kaiser(selection, beta=8.6) +twin = sfs.tapering.tukey(selection, alpha=0.3) # === compute synthesized sound field === p = sfs.fd.synthesize(d, twin, array, secondary_source, grid=grid) @@ -73,7 +73,7 @@ # === plot synthesized sound field === plt.figure(figsize=(10, 10)) -sfs.plot2d.amplitude(p, grid, [0, 0, 0]) +sfs.plot2d.amplitude(p, grid, xnorm=[0, 0, 0]) sfs.plot2d.loudspeakers(array.x, array.n, twin) plt.grid() plt.savefig('soundfield.png') diff --git a/doc/examples/time_domain.py b/doc/examples/time_domain.py index 858e2a7..9ad14a4 100644 --- a/doc/examples/time_domain.py +++ b/doc/examples/time_domain.py @@ -29,7 +29,7 @@ d = sfs.td.wfs.driving_signals(d_delay, d_weight, signal) # test soundfield -twin = sfs.tapering.tukey(selection, .3) +twin = sfs.tapering.tukey(selection, alpha=0.3) p = sfs.td.synthesize(d, twin, array, secondary_source, observation_time=t, grid=grid) @@ -54,7 +54,7 @@ d = sfs.td.wfs.driving_signals(d_delay, d_weight, signal) # test soundfield -twin = sfs.tapering.tukey(selection, .3) +twin = sfs.tapering.tukey(selection, alpha=0.3) p = sfs.td.synthesize(d, twin, array, secondary_source, observation_time=t, grid=grid) @@ -76,7 +76,7 @@ d = sfs.td.wfs.driving_signals(d_delay, d_weight, signal) # test soundfield -twin = sfs.tapering.tukey(selection, .3) +twin = sfs.tapering.tukey(selection, alpha=0.3) p = sfs.td.synthesize(d, twin, array, secondary_source, observation_time=t, grid=grid) p = p * 100 # scale absolute amplitude diff --git a/sfs/array.py b/sfs/array.py index f3ba7d6..045b8d8 100644 --- a/sfs/array.py +++ b/sfs/array.py @@ -84,7 +84,7 @@ def as_secondary_source_distribution(arg, **kwargs): return SecondarySourceDistribution(x, n, a) -def linear(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): +def linear(N, spacing, *, center=[0, 0, 0], orientation=[1, 0, 0]): """Return linear, equidistantly sampled secondary source distribution. Parameters @@ -119,7 +119,7 @@ def linear(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): return _linear_helper(np.arange(N) * spacing, center, orientation) -def linear_diff(distances, center=[0, 0, 0], orientation=[1, 0, 0]): +def linear_diff(distances, *, center=[0, 0, 0], orientation=[1, 0, 0]): """Return linear secondary source distribution from a list of distances. Parameters @@ -152,7 +152,7 @@ def linear_diff(distances, center=[0, 0, 0], orientation=[1, 0, 0]): return _linear_helper(ycoordinates, center, orientation) -def linear_random(N, min_spacing, max_spacing, center=[0, 0, 0], +def linear_random(N, min_spacing, max_spacing, *, center=[0, 0, 0], orientation=[1, 0, 0], seed=None): """Return randomly sampled linear array. @@ -190,10 +190,10 @@ def linear_random(N, min_spacing, max_spacing, center=[0, 0, 0], """ r = np.random.RandomState(seed) distances = r.uniform(min_spacing, max_spacing, size=N-1) - return linear_diff(distances, center, orientation) + return linear_diff(distances, center=center, orientation=orientation) -def circular(N, R, center=[0, 0, 0]): +def circular(N, R, *, center=[0, 0, 0]): """Return circular secondary source distribution parallel to the xy-plane. Parameters @@ -235,7 +235,7 @@ def circular(N, R, center=[0, 0, 0]): return SecondarySourceDistribution(positions, normals, weights) -def rectangular(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): +def rectangular(N, spacing, *, center=[0, 0, 0], orientation=[1, 0, 0]): """Return rectangular secondary source distribution. Parameters @@ -272,10 +272,14 @@ def rectangular(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): offset1 = spacing * (N2 - 1) / 2 + spacing / np.sqrt(2) offset2 = spacing * (N1 - 1) / 2 + spacing / np.sqrt(2) positions, normals, weights = concatenate( - linear(N1, spacing, [-offset1, 0, 0], [1, 0, 0]), # left - linear(N2, spacing, [0, offset2, 0], [0, -1, 0]), # upper - linear(N1, spacing, [offset1, 0, 0], [-1, 0, 0]), # right - linear(N2, spacing, [0, -offset2, 0], [0, 1, 0]), # lower + # left + linear(N1, spacing, center=[-offset1, 0, 0], orientation=[1, 0, 0]), + # upper + linear(N2, spacing, center=[0, offset2, 0], orientation=[0, -1, 0]), + # right + linear(N1, spacing, center=[offset1, 0, 0], orientation=[-1, 0, 0]), + # lower + linear(N2, spacing, center=[0, -offset2, 0], orientation=[0, 1, 0]), ) positions, normals = _rotate_array(positions, normals, [1, 0, 0], orientation) @@ -283,7 +287,7 @@ def rectangular(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): return SecondarySourceDistribution(positions, normals, weights) -def rounded_edge(Nxy, Nr, dx, center=[0, 0, 0], orientation=[1, 0, 0]): +def rounded_edge(Nxy, Nr, dx, *, center=[0, 0, 0], orientation=[1, 0, 0]): """Return SSD along the xy-axis with rounded edge at the origin. Parameters @@ -357,7 +361,7 @@ def rounded_edge(Nxy, Nr, dx, center=[0, 0, 0], orientation=[1, 0, 0]): return SecondarySourceDistribution(positions, directions, weights) -def edge(Nxy, dx, center=[0, 0, 0], orientation=[1, 0, 0]): +def edge(Nxy, dx, *, center=[0, 0, 0], orientation=[1, 0, 0]): """Return SSD along the xy-axis with sharp edge at the origin. Parameters @@ -409,7 +413,7 @@ def edge(Nxy, dx, center=[0, 0, 0], orientation=[1, 0, 0]): return SecondarySourceDistribution(positions, directions, weights) -def planar(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): +def planar(N, spacing, *, center=[0, 0, 0], orientation=[1, 0, 0]): """Return planar secondary source distribtion. Parameters @@ -460,7 +464,7 @@ def planar(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): return SecondarySourceDistribution(positions, normals, weights) -def cube(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): +def cube(N, spacing, *, center=[0, 0, 0], orientation=[1, 0, 0]): """Return cube-shaped secondary source distribtion. Parameters @@ -496,16 +500,23 @@ def cube(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): """ N1, N2, N3 = (N, N, N) if np.isscalar(N) else N - offset1 = spacing * (N2 - 1) / 2 + spacing / np.sqrt(2) - offset2 = spacing * (N1 - 1) / 2 + spacing / np.sqrt(2) - offset3 = spacing * (N3 - 1) / 2 + spacing / np.sqrt(2) + d = spacing + offset1 = d * (N2 - 1) / 2 + d / np.sqrt(2) + offset2 = d * (N1 - 1) / 2 + d / np.sqrt(2) + offset3 = d * (N3 - 1) / 2 + d / np.sqrt(2) positions, directions, weights = concatenate( - planar((N1, N3), spacing, [-offset1, 0, 0], [1, 0, 0]), # west - planar((N2, N3), spacing, [0, offset2, 0], [0, -1, 0]), # north - planar((N1, N3), spacing, [offset1, 0, 0], [-1, 0, 0]), # east - planar((N2, N3), spacing, [0, -offset2, 0], [0, 1, 0]), # south - planar((N2, N1), spacing, [0, 0, -offset3], [0, 0, 1]), # bottom - planar((N2, N1), spacing, [0, 0, offset3], [0, 0, -1]), # top + # west + planar((N1, N3), d, center=[-offset1, 0, 0], orientation=[1, 0, 0]), + # north + planar((N2, N3), d, center=[0, offset2, 0], orientation=[0, -1, 0]), + # east + planar((N1, N3), d, center=[offset1, 0, 0], orientation=[-1, 0, 0]), + # south + planar((N2, N3), d, center=[0, -offset2, 0], orientation=[0, 1, 0]), + # bottom + planar((N2, N1), d, center=[0, 0, -offset3], orientation=[0, 0, 1]), + # top + planar((N2, N1), d, center=[0, 0, offset3], orientation=[0, 0, -1]), ) positions, directions = _rotate_array(positions, directions, [1, 0, 0], orientation) @@ -513,7 +524,7 @@ def cube(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]): return SecondarySourceDistribution(positions, directions, weights) -def sphere_load(file, radius, center=[0, 0, 0]): +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 for the cartesian @@ -562,7 +573,7 @@ def sphere_load(file, radius, center=[0, 0, 0]): return SecondarySourceDistribution(positions, normals, weights) -def load(file, center=[0, 0, 0], orientation=[1, 0, 0]): +def load(file, *, center=[0, 0, 0], orientation=[1, 0, 0]): """Load secondary source distribution from file. Comma Separated Values (CSV) format with 7 values @@ -615,7 +626,7 @@ def load(file, center=[0, 0, 0], orientation=[1, 0, 0]): return SecondarySourceDistribution(positions, normals, weights) -def weights_midpoint(positions, closed): +def weights_midpoint(positions, *, closed): """Calculate loudspeaker weights for a simply connected array. The weights are calculated according to the midpoint rule. diff --git a/sfs/fd/__init__.py b/sfs/fd/__init__.py index ad3c0cc..20b7d0c 100644 --- a/sfs/fd/__init__.py +++ b/sfs/fd/__init__.py @@ -63,7 +63,7 @@ def secondary_source_point(omega, c): """Create a point source for use in `sfs.fd.synthesize()`.""" def secondary_source(position, _, grid): - return source.point(omega, position, grid, c) + return source.point(omega, position, grid, c=c) return secondary_source @@ -72,7 +72,7 @@ def secondary_source_line(omega, c): """Create a line source for use in `sfs.fd.synthesize()`.""" def secondary_source(position, _, grid): - return source.line(omega, position, grid, c) + return source.line(omega, position, grid, c=c) return secondary_source diff --git a/sfs/fd/esa.py b/sfs/fd/esa.py index 516acc7..583811e 100644 --- a/sfs/fd/esa.py +++ b/sfs/fd/esa.py @@ -16,8 +16,7 @@ from . import secondary_source_line, secondary_source_point -def plane_2d_edge(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, - c=None): +def plane_2d_edge(omega, x0, n=[0, 1, 0], *, alpha=3/2*np.pi, Nc=None, c=None): r"""Driving function for 2-dimensional plane wave with edge ESA. Driving function for a virtual plane wave using the 2-dimensional ESA @@ -87,8 +86,8 @@ def plane_2d_edge(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, return 4*np.pi/alpha * d, selection, secondary_source_line(omega, c) -def plane_2d_edge_dipole_ssd(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, - c=None): +def plane_2d_edge_dipole_ssd(omega, x0, n=[0, 1, 0], *, alpha=3/2*np.pi, + Nc=None, c=None): r"""Driving function for 2-dimensional plane wave with edge dipole ESA. Driving function for a virtual plane wave using the 2-dimensional ESA @@ -155,7 +154,7 @@ def plane_2d_edge_dipole_ssd(omega, x0, n=[0, 1, 0], alpha=3/2*np.pi, Nc=None, return 4*np.pi/alpha * d -def line_2d_edge(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): +def line_2d_edge(omega, x0, xs, *, alpha=3/2*np.pi, Nc=None, c=None): r"""Driving function for 2-dimensional line source with edge ESA. Driving function for a virtual line source using the 2-dimensional ESA @@ -229,7 +228,8 @@ def line_2d_edge(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): return -1j*np.pi/alpha * d, selection, secondary_source_line(omega, c) -def line_2d_edge_dipole_ssd(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): +def line_2d_edge_dipole_ssd(omega, x0, xs, *, alpha=3/2*np.pi, Nc=None, + c=None): r"""Driving function for 2-dimensional line source with edge dipole ESA. Driving function for a virtual line source using the 2-dimensional ESA @@ -300,8 +300,8 @@ def line_2d_edge_dipole_ssd(omega, x0, xs, alpha=3/2*np.pi, Nc=None, c=None): return -1j*np.pi/alpha * d -def point_25d_edge(omega, x0, xs, xref=[2, -2, 0], alpha=3/2*np.pi, - Nc=None, c=None): +def point_25d_edge(omega, x0, xs, *, xref=[2, -2, 0], alpha=3/2*np.pi, + Nc=None, c=None): r"""Driving function for 2.5-dimensional point source with edge ESA. Driving function for a virtual point source using the 2.5-dimensional diff --git a/sfs/fd/nfchoa.py b/sfs/fd/nfchoa.py index 55322a8..559ff03 100644 --- a/sfs/fd/nfchoa.py +++ b/sfs/fd/nfchoa.py @@ -35,7 +35,7 @@ def plot(d, selection, secondary_source): from . import secondary_source_point -def plane_2d(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): +def plane_2d(omega, x0, r0, n=[0, 1, 0], *, max_order=None, c=None): r"""Driving function for 2-dimensional NFC-HOA for a virtual plane wave. Parameters @@ -101,7 +101,7 @@ def plane_2d(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): return -2j / (np.pi*r0) * d, selection, secondary_source_point(omega, c) -def point_25d(omega, x0, r0, xs, max_order=None, c=None): +def point_25d(omega, x0, r0, xs, *, max_order=None, c=None): r"""Driving function for 2.5-dimensional NFC-HOA for a virtual point source. Parameters @@ -169,7 +169,7 @@ def point_25d(omega, x0, r0, xs, max_order=None, c=None): return d / (2 * np.pi * r0), selection, secondary_source_point(omega, c) -def plane_25d(omega, x0, r0, n=[0, 1, 0], max_order=None, c=None): +def plane_25d(omega, x0, r0, n=[0, 1, 0], *, max_order=None, c=None): r"""Driving function for 2.5-dimensional NFC-HOA for a virtual plane wave. Parameters diff --git a/sfs/fd/sdm.py b/sfs/fd/sdm.py index a14d7ed..fc9a583 100644 --- a/sfs/fd/sdm.py +++ b/sfs/fd/sdm.py @@ -34,7 +34,7 @@ def plot(d, selection, secondary_source): from . import secondary_source_line, secondary_source_point -def line_2d(omega, x0, n0, xs, c=None): +def line_2d(omega, x0, n0, xs, *, c=None): r"""Driving function for 2-dimensional SDM for a virtual line source. Parameters @@ -87,7 +87,7 @@ def line_2d(omega, x0, n0, xs, c=None): return d, selection, secondary_source_line(omega, c) -def plane_2d(omega, x0, n0, n=[0, 1, 0], c=None): +def plane_2d(omega, x0, n0, n=[0, 1, 0], *, c=None): r"""Driving function for 2-dimensional SDM for a virtual plane wave. Parameters @@ -142,7 +142,7 @@ def plane_2d(omega, x0, n0, n=[0, 1, 0], c=None): return d, selection, secondary_source_line(omega, c) -def plane_25d(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): +def plane_25d(omega, x0, n0, n=[0, 1, 0], *, xref=[0, 0, 0], c=None): r"""Driving function for 2.5-dimensional SDM for a virtual plane wave. Parameters @@ -182,7 +182,7 @@ def plane_25d(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): :context: close-figs d, selection, secondary_source = sfs.fd.sdm.plane_25d( - omega, array.x, array.n, npw, [0, -1, 0]) + omega, array.x, array.n, npw, xref=[0, -1, 0]) plot(d, selection, secondary_source) """ @@ -197,7 +197,7 @@ def plane_25d(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): return d, selection, secondary_source_point(omega, c) -def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None): +def point_25d(omega, x0, n0, xs, *, xref=[0, 0, 0], c=None): r"""Driving function for 2.5-dimensional SDM for a virtual point source. Parameters @@ -237,7 +237,7 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None): :context: close-figs d, selection, secondary_source = sfs.fd.sdm.point_25d( - omega, array.x, array.n, xs, [0, -1, 0]) + omega, array.x, array.n, xs, xref=[0, -1, 0]) plot(d, selection, secondary_source) """ diff --git a/sfs/fd/source.py b/sfs/fd/source.py index 568317f..993bcba 100644 --- a/sfs/fd/source.py +++ b/sfs/fd/source.py @@ -32,7 +32,7 @@ from .. import default -def point(omega, x0, grid, c=None): +def point(omega, x0, grid, *, c=None): r"""Sound pressure of a point source. Parameters @@ -85,7 +85,7 @@ def point(omega, x0, grid, c=None): return 1 / (4*np.pi) * np.exp(-1j * k * r) / r -def point_velocity(omega, x0, grid, c=None, rho0=None): +def point_velocity(omega, x0, grid, *, c=None, rho0=None): """Particle velocity of a point source. Parameters @@ -134,7 +134,7 @@ def point_velocity(omega, x0, grid, c=None, rho0=None): return util.XyzComponents([v * o / r for o in offset]) -def point_averaged_intensity(omega, x0, grid, c=None, rho0=None): +def point_averaged_intensity(omega, x0, grid, *, c=None, rho0=None): """Velocity of a point source. Parameters @@ -169,7 +169,7 @@ def point_averaged_intensity(omega, x0, grid, c=None, rho0=None): return util.XyzComponents([i * o / r**2 for o in offset]) -def point_dipole(omega, x0, n0, grid, c=None): +def point_dipole(omega, x0, n0, grid, *, c=None): r"""Point source with dipole characteristics. Parameters @@ -221,7 +221,7 @@ def point_dipole(omega, x0, n0, grid, c=None): np.power(r, 2) * np.exp(-1j * k * r) -def point_modal(omega, x0, grid, L, N=None, deltan=0, c=None): +def point_modal(omega, x0, grid, L, *, N=None, deltan=0, c=None): """Point source in a rectangular room using a modal room model. Parameters @@ -286,7 +286,7 @@ def point_modal(omega, x0, grid, L, N=None, deltan=0, c=None): return p -def point_modal_velocity(omega, x0, grid, L, N=None, deltan=0, c=None): +def point_modal_velocity(omega, x0, grid, L, *, N=None, deltan=0, c=None): """Velocity of point source in a rectangular room using a modal room model. Parameters @@ -358,7 +358,7 @@ def point_modal_velocity(omega, x0, grid, L, N=None, deltan=0, c=None): return util.XyzComponents([vx, vy, vz]) -def point_image_sources(omega, x0, grid, L, max_order, coeffs=None, c=None): +def point_image_sources(omega, x0, grid, L, *, max_order, coeffs=None, c=None): """Point source in a rectangular room using the mirror image source model. Parameters @@ -395,12 +395,12 @@ def point_image_sources(omega, x0, grid, L, max_order, coeffs=None, c=None): p = 0 for position, strength in zip(xs, source_strengths): if strength != 0: - p += strength * point(omega, position, grid, c) + p += strength * point(omega, position, grid, c=c) return p -def line(omega, x0, grid, c=None): +def line(omega, x0, grid, *, c=None): r"""Line source parallel to the z-axis. Note: third component of x0 is ignored. @@ -439,7 +439,7 @@ def line(omega, x0, grid, c=None): return _duplicate_zdirection(p, grid) -def line_velocity(omega, x0, grid, c=None, rho0=None): +def line_velocity(omega, x0, grid, *, c=None, rho0=None): """Velocity of line source parallel to the z-axis. Returns @@ -481,7 +481,7 @@ def line_velocity(omega, x0, grid, c=None, rho0=None): return util.XyzComponents([_duplicate_zdirection(vi, grid) for vi in v]) -def line_dipole(omega, x0, n0, grid, c=None): +def line_dipole(omega, x0, n0, grid, *, c=None): r"""Line source with dipole characteristics parallel to the z-axis. Note: third component of x0 is ignored. @@ -504,7 +504,7 @@ def line_dipole(omega, x0, n0, grid, c=None): return _duplicate_zdirection(p, grid) -def line_dirichlet_edge(omega, x0, grid, alpha=3/2*np.pi, Nc=None, c=None): +def line_dirichlet_edge(omega, x0, grid, *, alpha=3/2*np.pi, Nc=None, c=None): """Line source scattered at an edge with Dirichlet boundary conditions. :cite:`Moser2012`, eq.(10.18/19) @@ -570,7 +570,7 @@ def line_dirichlet_edge(omega, x0, grid, alpha=3/2*np.pi, Nc=None, c=None): return p -def plane(omega, x0, n0, grid, c=None): +def plane(omega, x0, n0, grid, *, c=None): r"""Plane wave. Parameters @@ -617,7 +617,7 @@ def plane(omega, x0, n0, grid, c=None): return np.exp(-1j * k * np.inner(grid - x0, n0)) -def plane_velocity(omega, x0, n0, grid, c=None, rho0=None): +def plane_velocity(omega, x0, n0, grid, *, c=None, rho0=None): r"""Velocity of a plane wave. Parameters @@ -668,7 +668,7 @@ def plane_velocity(omega, x0, n0, grid, c=None, rho0=None): return util.XyzComponents([v * n for n in n0]) -def plane_averaged_intensity(omega, x0, n0, grid, c=None, rho0=None): +def plane_averaged_intensity(omega, x0, n0, grid, *, c=None, rho0=None): r"""Averaged intensity of a plane wave. Parameters @@ -707,7 +707,7 @@ def plane_averaged_intensity(omega, x0, n0, grid, c=None, rho0=None): return util.XyzComponents([i * n for n in n0]) -def pulsating_sphere(omega, center, radius, amplitude, grid, inside=False, +def pulsating_sphere(omega, center, radius, amplitude, grid, *, inside=False, c=None): """Sound pressure of a pulsating sphere. @@ -765,7 +765,8 @@ def pulsating_sphere(omega, center, radius, amplitude, grid, inside=False, return impedance * radial_velocity -def pulsating_sphere_velocity(omega, center, radius, amplitude, grid, c=None): +def pulsating_sphere_velocity(omega, center, radius, amplitude, grid, *, + c=None): """Particle velocity of a pulsating sphere. Parameters diff --git a/sfs/fd/wfs.py b/sfs/fd/wfs.py index 66a5fea..deaf7e9 100644 --- a/sfs/fd/wfs.py +++ b/sfs/fd/wfs.py @@ -39,7 +39,7 @@ def plot(d, selection, secondary_source): from . import secondary_source_line, secondary_source_point -def line_2d(omega, x0, n0, xs, c=None): +def line_2d(omega, x0, n0, xs, *, c=None): r"""Driving function for 2-dimensional WFS for a virtual line source. Parameters @@ -95,7 +95,7 @@ def line_2d(omega, x0, n0, xs, c=None): return d, selection, secondary_source_line(omega, c) -def _point(omega, x0, n0, xs, c=None): +def _point(omega, x0, n0, xs, *, c=None): r"""Driving function for 2/3-dimensional WFS for a virtual point source. Parameters @@ -322,7 +322,7 @@ def point_25d_legacy(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): return d, selection, secondary_source_point(omega, c) -def _plane(omega, x0, n0, n=[0, 1, 0], c=None): +def _plane(omega, x0, n0, n=[0, 1, 0], *, c=None): r"""Driving function for 2/3-dimensional WFS for a virtual plane wave. Parameters @@ -380,7 +380,7 @@ def _plane(omega, x0, n0, n=[0, 1, 0], c=None): plane_2d = _plane -def plane_25d(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, +def plane_25d(omega, x0, n0, n=[0, 1, 0], *, xref=[0, 0, 0], c=None, omalias=None): r"""Driving function for 2.5-dimensional WFS for a virtual plane wave. @@ -446,7 +446,7 @@ def plane_25d(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, plane_3d = _plane -def _focused(omega, x0, n0, xs, ns, c=None): +def _focused(omega, x0, n0, xs, ns, *, c=None): r"""Driving function for 2/3-dimensional WFS for a focused source. Parameters @@ -507,7 +507,7 @@ def _focused(omega, x0, n0, xs, ns, c=None): focused_2d = _focused -def focused_25d(omega, x0, n0, xs, ns, xref=[0, 0, 0], c=None, +def focused_25d(omega, x0, n0, xs, ns, *, xref=[0, 0, 0], c=None, omalias=None): r"""Driving function for 2.5-dimensional WFS for a focused source. @@ -615,7 +615,7 @@ def preeq_25d(omega, omalias, c): return np.sqrt(1j * util.wavenumber(omalias, c)) -def plane_3d_delay(omega, x0, n0, n=[0, 1, 0], c=None): +def plane_3d_delay(omega, x0, n0, n=[0, 1, 0], *, c=None): r"""Delay-only driving function for a virtual plane wave. Parameters @@ -666,7 +666,7 @@ def plane_3d_delay(omega, x0, n0, n=[0, 1, 0], c=None): return d, selection, secondary_source_point(omega, c) -def soundfigure_3d(omega, x0, n0, figure, npw=[0, 0, 1], c=None): +def soundfigure_3d(omega, x0, n0, figure, npw=[0, 0, 1], *, c=None): """Compute driving function for a 2D sound figure. Based on @@ -701,7 +701,7 @@ def soundfigure_3d(omega, x0, n0, figure, npw=[0, 0, 1], c=None): npw = npw / np.linalg.norm(npw) # driving function of plane wave with positive kz d_component, selection, secondary_source = plane_3d( - omega, x0, n0, npw, c) + omega, x0, n0, npw, c=c) d += selection * figure[n, m] * d_component return d, util.source_selection_all(len(d)), secondary_source diff --git a/sfs/plot2d.py b/sfs/plot2d.py index fd76a36..9efba27 100644 --- a/sfs/plot2d.py +++ b/sfs/plot2d.py @@ -41,7 +41,7 @@ def _register_cmap_transparent(name, color): _register_cmap_transparent('blacktransparent', 'black') -def virtualsource(xs, ns=None, type='point', ax=None): +def virtualsource(xs, ns=None, type='point', *, ax=None): """Draw position/orientation of virtual source.""" xs = np.asarray(xs) ns = np.asarray(ns) @@ -61,7 +61,7 @@ def virtualsource(xs, ns=None, type='point', ax=None): head_length=0.1, fc='k', ec='k') -def reference(xref, size=0.1, ax=None): +def reference(xref, *, size=0.1, ax=None): """Draw reference/normalization point.""" xref = np.asarray(xref) if ax is None: @@ -71,7 +71,7 @@ def reference(xref, size=0.1, ax=None): ax.plot((xref[0]-size, xref[0]+size), (xref[1]+size, xref[1]-size), 'k-') -def secondary_sources(x0, n0, grid=None): +def secondary_sources(x0, n0, *, grid=None): """Simple plot of secondary source locations.""" x0 = np.asarray(x0) n0 = np.asarray(n0) @@ -87,7 +87,7 @@ def secondary_sources(x0, n0, grid=None): ax.add_artist(ss) -def loudspeakers(x0, n0, a0=0.5, size=0.08, show_numbers=False, grid=None, +def loudspeakers(x0, n0, a0=0.5, *, size=0.08, show_numbers=False, grid=None, ax=None): """Draw loudspeaker symbols at given locations and angles. @@ -164,9 +164,9 @@ def _visible_secondarysources(x0, n0, grid): return x0[idx, :], n0[idx, :] -def amplitude(p, grid, xnorm=None, cmap='coolwarm_clip', vmin=-2.0, vmax=2.0, - xlabel=None, ylabel=None, colorbar=True, colorbar_kwargs={}, - ax=None, **kwargs): +def amplitude(p, grid, *, xnorm=None, cmap='coolwarm_clip', + vmin=-2.0, vmax=2.0, xlabel=None, ylabel=None, + colorbar=True, colorbar_kwargs={}, ax=None, **kwargs): """Two-dimensional plot of sound field (real part). Parameters @@ -294,7 +294,7 @@ def amplitude(p, grid, xnorm=None, cmap='coolwarm_clip', vmin=-2.0, vmax=2.0, return im -def level(p, grid, xnorm=None, power=False, cmap=None, vmax=3, vmin=-50, +def level(p, grid, *, xnorm=None, power=False, cmap=None, vmax=3, vmin=-50, **kwargs): """Two-dimensional plot of level (dB) of sound field. @@ -314,7 +314,7 @@ def level(p, grid, xnorm=None, power=False, cmap=None, vmax=3, vmin=-50, vmax=vmax, vmin=vmin, **kwargs) -def particles(x, trim=None, ax=None, xlabel='x (m)', ylabel='y (m)', +def particles(x, *, trim=None, ax=None, xlabel='x (m)', ylabel='y (m)', edgecolor='', marker='.', s=15, **kwargs): """Plot particle positions as scatter plot""" XX, YY = [np.real(c) for c in x[:2]] @@ -337,8 +337,8 @@ def particles(x, trim=None, ax=None, xlabel='x (m)', ylabel='y (m)', **kwargs) -def vectors(v, grid, cmap='blacktransparent', headlength=3, headaxislength=2.5, - ax=None, clim=None, **kwargs): +def vectors(v, grid, *, cmap='blacktransparent', headlength=3, + headaxislength=2.5, ax=None, clim=None, **kwargs): """Plot a vector field in the xy plane. Parameters @@ -384,7 +384,7 @@ def vectors(v, grid, cmap='blacktransparent', headlength=3, headaxislength=2.5, headaxislength=headaxislength, clim=clim, **kwargs) -def add_colorbar(im, aspect=20, pad=0.5, **kwargs): +def add_colorbar(im, *, aspect=20, pad=0.5, **kwargs): r"""Add a vertical color bar to a plot. Parameters diff --git a/sfs/plot3d.py b/sfs/plot3d.py index 9cf4cb0..617da51 100644 --- a/sfs/plot3d.py +++ b/sfs/plot3d.py @@ -3,7 +3,7 @@ from mpl_toolkits.mplot3d import Axes3D -def secondary_sources(x0, n0, a0=None, w=0.08, h=0.08): +def secondary_sources(x0, n0, a0=None, *, w=0.08, h=0.08): """Plot positions and normals of a 3D secondary source distribution.""" fig = plt.figure(figsize=(15, 15)) ax = fig.add_subplot(111, projection='3d') diff --git a/sfs/tapering.py b/sfs/tapering.py index 1541eef..9b34c8f 100644 --- a/sfs/tapering.py +++ b/sfs/tapering.py @@ -51,7 +51,7 @@ def none(active): return active -def tukey(active, alpha): +def tukey(active, *, alpha): """Tukey tapering window. This uses a function similar to :func:`scipy.signal.tukey`, except @@ -75,18 +75,18 @@ def tukey(active, alpha): .. plot:: :context: close-figs - plt.plot(sfs.tapering.tukey(active1, 0), label='alpha = 0') - plt.plot(sfs.tapering.tukey(active1, 0.25), label='alpha = 0.25') - plt.plot(sfs.tapering.tukey(active1, 0.5), label='alpha = 0.5') - plt.plot(sfs.tapering.tukey(active1, 0.75), label='alpha = 0.75') - plt.plot(sfs.tapering.tukey(active1, 1), label='alpha = 1') + plt.plot(sfs.tapering.tukey(active1, alpha=0), label='alpha = 0') + plt.plot(sfs.tapering.tukey(active1, alpha=0.25), label='alpha = 0.25') + plt.plot(sfs.tapering.tukey(active1, alpha=0.5), label='alpha = 0.5') + plt.plot(sfs.tapering.tukey(active1, alpha=0.75), label='alpha = 0.75') + plt.plot(sfs.tapering.tukey(active1, alpha=1), label='alpha = 1') plt.axis([-3, 103, -0.1, 1.1]) plt.legend(loc='lower center') .. plot:: :context: close-figs - plt.plot(sfs.tapering.tukey(active2, 0.3)) + plt.plot(sfs.tapering.tukey(active2, alpha=0.3)) plt.axis([-3, 103, -0.1, 1.1]) """ @@ -109,7 +109,7 @@ def tukey(active, alpha): return result -def kaiser(active, beta): +def kaiser(active, *, beta): """Kaiser tapering window. This uses :func:`numpy.kaiser`. @@ -131,18 +131,18 @@ def kaiser(active, beta): .. plot:: :context: close-figs - plt.plot(sfs.tapering.kaiser(active1, 0), label='beta = 0') - plt.plot(sfs.tapering.kaiser(active1, 2), label='beta = 2') - plt.plot(sfs.tapering.kaiser(active1, 6), label='beta = 6') - plt.plot(sfs.tapering.kaiser(active1, 8.6), label='beta = 8.6') - plt.plot(sfs.tapering.kaiser(active1, 14), label='beta = 14') + plt.plot(sfs.tapering.kaiser(active1, beta=0), label='beta = 0') + plt.plot(sfs.tapering.kaiser(active1, beta=2), label='beta = 2') + plt.plot(sfs.tapering.kaiser(active1, beta=6), label='beta = 6') + plt.plot(sfs.tapering.kaiser(active1, beta=8.6), label='beta = 8.6') + plt.plot(sfs.tapering.kaiser(active1, beta=14), label='beta = 14') plt.axis([-3, 103, -0.1, 1.1]) plt.legend(loc='lower center') .. plot:: :context: close-figs - plt.plot(sfs.tapering.kaiser(active2, 7)) + plt.plot(sfs.tapering.kaiser(active2, beta=7)) plt.axis([-3, 103, -0.1, 1.1]) """ diff --git a/sfs/util.py b/sfs/util.py index 22a4185..b25ff83 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -230,7 +230,8 @@ def as_delayed_signal(arg, **kwargs): raise TypeError('expected audio data, samplerate, optional start time') -def strict_arange(start, stop, step=1, endpoint=False, dtype=None, **kwargs): +def strict_arange(start, stop, step=1, *, endpoint=False, dtype=None, + **kwargs): """Like :func:`numpy.arange`, but compensating numeric errors. Unlike :func:`numpy.arange`, but similar to :func:`numpy.linspace`, @@ -266,7 +267,7 @@ def strict_arange(start, stop, step=1, endpoint=False, dtype=None, **kwargs): return np.arange(start, stop, step, dtype) -def xyz_grid(x, y, z, spacing, endpoint=True, **kwargs): +def xyz_grid(x, y, z, *, spacing, endpoint=True, **kwargs): """Create a grid with given range and spacing. Parameters @@ -349,7 +350,7 @@ def displacement(v, omega): return as_xyz_components(v) / (1j * omega) -def db(x, power=False): +def db(x, *, power=False): """Convert *x* to decibel. Parameters @@ -477,7 +478,7 @@ def apply(self, func, *args, **kwargs): """ -def image_sources_for_box(x, L, N, prune=True): +def image_sources_for_box(x, L, N, *, prune=True): """Image source method for a cuboid room. The classical method by Allen and Berkley :cite:`Allen1979`. From 263cb93578971c146487b6fad6abb1982ffeb4f4 Mon Sep 17 00:00:00 2001 From: Hagen Wierstorf Date: Sat, 16 Mar 2019 16:13:39 +0100 Subject: [PATCH 070/101] DOC: Update existing links to theory --- doc/conf.py | 4 ++++ sfs/fd/nfchoa.py | 6 +++--- sfs/td/wfs.py | 12 +++--------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index f4e4735..a319efa 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -41,6 +41,7 @@ 'sphinx.ext.intersphinx', 'sphinx.ext.doctest', 'sphinxcontrib.bibtex', + 'sphinx.ext.extlinks', 'matplotlib.sphinxext.plot_directive', 'nbsphinx', ] @@ -77,6 +78,9 @@ 'matplotlib': ('https://matplotlib.org/', None), } +extlinks = {'sfs': ('https://sfs.readthedocs.io/en/3.2/%s', + 'https://sfs.rtfd.io/')} + plot_include_source = True plot_html_show_source_link = False plot_html_show_formats = False diff --git a/sfs/fd/nfchoa.py b/sfs/fd/nfchoa.py index 559ff03..0530034 100644 --- a/sfs/fd/nfchoa.py +++ b/sfs/fd/nfchoa.py @@ -74,7 +74,7 @@ def plane_2d(omega, x0, r0, n=[0, 1, 0], *, max_order=None, c=None): \frac{\i^{-m}}{\Hankel{2}{m}{\wc r_0}} \e{\i m (\phi_0 - \phi_\text{pw})} - See http://sfstoolbox.org/#equation-D.nfchoa.pw.2D. + See :sfs:`d_nfchoa/#equation-fd-nfchoa-plane-2d` Examples -------- @@ -140,7 +140,7 @@ def point_25d(omega, x0, r0, xs, *, max_order=None, c=None): \frac{\hankel{2}{|m|}{\wc r}}{\hankel{2}{|m|}{\wc r_0}} \e{\i m (\phi_0 - \phi)} - See http://sfstoolbox.org/#equation-D.nfchoa.ps.2.5D. + See :sfs:`d_nfchoa/#equation-fd-nfchoa-point-25d` Examples -------- @@ -208,7 +208,7 @@ def plane_25d(omega, x0, r0, n=[0, 1, 0], *, max_order=None, c=None): \frac{\i^{-|m|}}{\wc \hankel{2}{|m|}{\wc r_0}} \e{\i m (\phi_0 - \phi_\text{pw})} - See http://sfstoolbox.org/#equation-D.nfchoa.pw.2.5D. + See :sfs:`d_nfchoa/#equation-fd-nfchoa-plane-25d` Examples -------- diff --git a/sfs/td/wfs.py b/sfs/td/wfs.py index 03fa985..36e95fd 100644 --- a/sfs/td/wfs.py +++ b/sfs/td/wfs.py @@ -96,9 +96,7 @@ def plane_25d(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): with wfs(2.5D) prefilter h(t), which is not implemented yet. - References - ---------- - See http://sfstoolbox.org/en/latest/#equation-d.wfs.pw.2.5D + See :sfs:`d_wfs/#equation-td-wfs-plane-25d` Examples -------- @@ -173,9 +171,7 @@ def point_25d(x0, n0, xs, xref=[0, 0, 0], c=None): with wfs(2.5D) prefilter h(t), which is not implemented yet. - References - ---------- - See http://sfstoolbox.org/en/latest/#equation-d.wfs.ps.2.5D + See :sfs:`d_wfs/#equation-td-wfs-point-25d` Examples -------- @@ -257,9 +253,7 @@ def focused_25d(x0, n0, xs, ns, xref=[0, 0, 0], c=None): with wfs(2.5D) prefilter h(t), which is not implemented yet. - References - ---------- - See http://sfstoolbox.org/en/latest/#equation-d.wfs.fs.2.5D + See :sfs:`d_wfs/#equation-td-wfs-focused-25d` Examples -------- From 4807ff870ad77bb28b7b537890eaae2599ef5b04 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 16 Mar 2019 12:32:31 +0100 Subject: [PATCH 071/101] Move sfs.util.displacement() to sfs.fd.displacement() Closes #30. --- doc/examples/animations-pulsating-sphere.ipynb | 2 +- doc/examples/animations_pulsating_sphere.py | 2 +- doc/examples/plot_particle_density.py | 2 +- sfs/fd/__init__.py | 12 ++++++++++++ sfs/util.py | 11 ----------- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/doc/examples/animations-pulsating-sphere.ipynb b/doc/examples/animations-pulsating-sphere.ipynb index e9efc73..f8b1e22 100644 --- a/doc/examples/animations-pulsating-sphere.ipynb +++ b/doc/examples/animations-pulsating-sphere.ipynb @@ -33,7 +33,7 @@ "\n", "while the last one can be obtained by using\n", "\n", - "- [sfs.util.displacement()](../sfs.util.rst#sfs.util.displacement)\n", + "- [sfs.fd.displacement()](../sfs.fd.rst#sfs.fd.displacement)\n", "\n", "which converts the particle velocity into displacement.\n", "\n", diff --git a/doc/examples/animations_pulsating_sphere.py b/doc/examples/animations_pulsating_sphere.py index 9fb741d..a415067 100644 --- a/doc/examples/animations_pulsating_sphere.py +++ b/doc/examples/animations_pulsating_sphere.py @@ -10,7 +10,7 @@ def particle_displacement(omega, center, radius, amplitude, grid, frames, """Generate sound particle animation.""" velocity = sfs.fd.source.pulsating_sphere_velocity( omega, center, radius, amplitude, grid) - displacement = sfs.util.displacement(velocity, omega) + displacement = sfs.fd.displacement(velocity, omega) phasor = np.exp(1j * 2 * np.pi / frames) fig, ax = plt.subplots(figsize=figsize) diff --git a/doc/examples/plot_particle_density.py b/doc/examples/plot_particle_density.py index 9de8845..b21cd20 100644 --- a/doc/examples/plot_particle_density.py +++ b/doc/examples/plot_particle_density.py @@ -19,7 +19,7 @@ def plot_particle_displacement(title): # compute displacement - X = grid + amplitude * sfs.util.displacement(v, omega) + X = grid + amplitude * sfs.fd.displacement(v, omega) # plot displacement plt.figure(figsize=(15, 15)) plt.cla() diff --git a/sfs/fd/__init__.py b/sfs/fd/__init__.py index 20b7d0c..89b9e1e 100644 --- a/sfs/fd/__init__.py +++ b/sfs/fd/__init__.py @@ -13,6 +13,7 @@ """ from . import source from .. import array as _array +from .. import util as _util import numpy as _np @@ -22,6 +23,17 @@ def shiftphase(p, phase): return p * _np.exp(1j * phase) +def displacement(v, omega): + r"""Particle displacement. + + .. math:: + + d(x, t) = \int_{-\infty}^t v(x, \tau) d\tau + + """ + return _util.as_xyz_components(v) / (1j * omega) + + def synthesize(d, weights, ssd, secondary_source_function, **kwargs): """Compute sound field for a generic driving function. diff --git a/sfs/util.py b/sfs/util.py index b25ff83..b054346 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -339,17 +339,6 @@ def normalize_vector(x): return x / np.linalg.norm(x) -def displacement(v, omega): - r"""Particle displacement. - - .. math:: - - d(x, t) = \int_{-\infty}^t v(x, \tau) d\tau - - """ - return as_xyz_components(v) / (1j * omega) - - def db(x, *, power=False): """Convert *x* to decibel. From 12805a154a3a357711751525f7dbfda384584368 Mon Sep 17 00:00:00 2001 From: Hagen Wierstorf Date: Mon, 18 Mar 2019 10:50:00 +0100 Subject: [PATCH 072/101] DOC: Add sound field synthesis example as notebook --- doc/example-python-scripts.rst | 2 - doc/examples.rst | 1 + doc/examples/sound-field-synthesis.ipynb | 232 +++++++++++++++++++++++ doc/examples/sound_field_synthesis.py | 83 -------- 4 files changed, 233 insertions(+), 85 deletions(-) create mode 100644 doc/examples/sound-field-synthesis.ipynb delete mode 100644 doc/examples/sound_field_synthesis.py diff --git a/doc/example-python-scripts.rst b/doc/example-python-scripts.rst index 85b0436..2e38f11 100644 --- a/doc/example-python-scripts.rst +++ b/doc/example-python-scripts.rst @@ -3,8 +3,6 @@ Example Python Scripts Various example scripts are located in the directory ``doc/examples/``, e.g. -* :download:`examples/sound_field_synthesis.py`: Illustrates the general usage - of the toolbox * :download:`examples/horizontal_plane_arrays.py`: Computes the sound fields for various techniques, virtual sources and loudspeaker array configurations * :download:`examples/animations_pulsating_sphere.py`: Creates animations of a diff --git a/doc/examples.rst b/doc/examples.rst index 3a20de9..0248604 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -13,6 +13,7 @@ Examples .. toctree:: :maxdepth: 1 + examples/sound-field-synthesis examples/modal-room-acoustics examples/mirror-image-source-model examples/animations-pulsating-sphere diff --git a/doc/examples/sound-field-synthesis.ipynb b/doc/examples/sound-field-synthesis.ipynb new file mode 100644 index 0000000..89f298f --- /dev/null +++ b/doc/examples/sound-field-synthesis.ipynb @@ -0,0 +1,232 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Sound Field Synthesis\n", + "\n", + "Illustrates the usage of the SFS toolbox for the simulation of different sound field synthesis methods." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt \n", + "import sfs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Simulation parameters\n", + "number_of_secondary_sources = 56\n", + "frequency = 680 # in Hz\n", + "pw_angle = 30 # traveling direction of plane wave in degree\n", + "xs = [-2, -1, 0] # position of virtual point source in m\n", + "\n", + "grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02)\n", + "omega = 2 * np.pi * frequency # angular frequency\n", + "npw = sfs.util.direction_vector(np.radians(pw_angle)) # normal vector of plane wave" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Define a helper function for synthesize and plot the sound field from the given driving signals." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def sound_field(d, selection, secondary_source, array, grid, tapering=True):\n", + " if tapering:\n", + " tapering_window = sfs.tapering.tukey(selection, alpha=0.3)\n", + " else:\n", + " tapering_window = sfs.tapering.none(selection)\n", + " p = sfs.fd.synthesize(d, tapering_window, array, secondary_source, grid=grid)\n", + " sfs.plot2d.amplitude(p, grid, xnorm=[0, 0, 0])\n", + " sfs.plot2d.loudspeakers(array.x, array.n, tapering_window)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Circular loudspeaker arrays\n", + "\n", + "In the following we show different sound field synthesis methods applied to a circular loudspeaker array." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "radius = 1.5 # in m\n", + "array = sfs.array.circular(number_of_secondary_sources, radius)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Wave Field Synthesis (WFS)\n", + "\n", + "#### Plane wave" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "d, selection, secondary_source = sfs.fd.wfs.plane_25d(omega, array.x, array.n, n=npw)\n", + "sound_field(d, selection, secondary_source, array, grid)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Point source" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "d, selection, secondary_source = sfs.fd.wfs.point_25d(omega, array.x, array.n, xs)\n", + "sound_field(d, selection, secondary_source, array, grid)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Near-Field Compensated Higher Order Ambisonics (NFC-HOA)\n", + "\n", + "#### Plane wave" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "d, selection, secondary_source = sfs.fd.nfchoa.plane_25d(omega, array.x, radius, n=npw)\n", + "sound_field(d, selection, secondary_source, array, grid, tapering=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Point source" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "d, selection, secondary_source = sfs.fd.nfchoa.point_25d(omega, array.x, radius, xs)\n", + "sound_field(d, selection, secondary_source, array, grid, tapering=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Linear loudspeaker array\n", + "\n", + "In the following we show different sound field synthesis methods applied to a linear loudspeaker array." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "spacing = 0.07 # in m\n", + "array = sfs.array.linear(number_of_secondary_sources, spacing,\n", + " center=[0, -0.5, 0], orientation=[0, 1, 0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Wave Field Synthesis (WFS)\n", + "\n", + "#### Plane wave" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "d, selection, secondary_source = sfs.fd.wfs.plane_25d(omega, array.x, array.n, npw)\n", + "sound_field(d, selection, secondary_source, array, grid)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Point source" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "d, selection, secondary_source = sfs.fd.wfs.point_25d(omega, array.x, array.n, xs)\n", + "sound_field(d, selection, secondary_source, array, grid)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/doc/examples/sound_field_synthesis.py b/doc/examples/sound_field_synthesis.py deleted file mode 100644 index dc8125d..0000000 --- a/doc/examples/sound_field_synthesis.py +++ /dev/null @@ -1,83 +0,0 @@ -""" - Illustrates the usage of the SFS toolbox for the simulation of SFS. - - This script contains almost all possibilities than can be used - for the synthesis of sound fields generated by Wave Field Synthesis or - Higher-Order Ambisonics with various loudspeaker configurations. -""" - -import numpy as np -import matplotlib.pyplot as plt -import sfs - - -# simulation parameters -dx = 0.2 # secondary source distance -N = 16 # number of secondary sources -pw_angle = 30 # traveling direction of plane wave -xs = [2, 1, 0] # position of virtual source -xref = [0, 0, 0] # reference position for 2.5D -f = 680 # frequency -R = 1.5 # radius of spherical/circular array - -grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02) - -# angular frequency -omega = 2 * np.pi * f -# normal vector of plane wave -npw = sfs.util.direction_vector(np.radians(pw_angle)) - - -# === 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/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)) - -#array = sfs.array.sphere_load('/Users/spors/Documents/src/SFS/data/spherical_grids/equally_spaced_points/006561points.mat', 1, center=[.5,0,0]) - - -# === compute driving function and determine active secondary sources === -#d, selection, secondary_source = sfs.fd.wfs.plane_3d_delay(omega, array.x, array.n, npw) - -#d, selection, secondary_source = sfs.fd.wfs.line_2d(omega, array.x, array.n, xs) - -#d, selection, secondary_source = sfs.fd.wfs.plane_2d(omega, array.x, array.n, npw) -d, selection, secondary_source = sfs.fd.wfs.plane_25d(omega, array.x, array.n, npw, xref=xref) -#d, selection, secondary_source = sfs.fd.wfs.plane_3d(omega, array.x, array.n, npw) - -#d, selection, secondary_source = sfs.fd.wfs.point_2d(omega, array.x, array.n, xs) -#d, selection, secondary_source = sfs.fd.wfs.point_25d(omega, array.x, array.n, xs) -#d, selection, secondary_source = sfs.fd.wfs.point_3d(omega, array.x, array.n, xs) - -#d, selection, secondary_source = sfs.fd.nfchoa.plane_2d(omega, array.x, R, npw) - -#d, selection, secondary_source = sfs.fd.nfchoa.point_25d(omega, array.x, R, xs) -#d, selection, secondary_source = sfs.fd.nfchoa.plane_25d(omega, array.x, R, npw) - - -# === compute tapering window === -#twin = sfs.tapering.none(selection) -#twin = sfs.tapering.kaiser(selection, beta=8.6) -twin = sfs.tapering.tukey(selection, alpha=0.3) - -# === compute synthesized sound field === -p = sfs.fd.synthesize(d, twin, array, secondary_source, grid=grid) - - -# === plot synthesized sound field === -plt.figure(figsize=(10, 10)) -sfs.plot2d.amplitude(p, grid, xnorm=[0, 0, 0]) -sfs.plot2d.loudspeakers(array.x, array.n, twin) -plt.grid() -plt.savefig('soundfield.png') - - -#sfs.plot3d.secondary_sources(array.x, array.n, twin) -#plt.savefig('loudspeakers.png') From 725d1a723e5bbc092df08364444d5a3ffb1f94b3 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 13 Mar 2019 19:33:28 +0100 Subject: [PATCH 073/101] Use \mathop instead of \operatorname --- doc/math-definitions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/math-definitions.rst b/doc/math-definitions.rst index f5bba74..cec4c17 100644 --- a/doc/math-definitions.rst +++ b/doc/math-definitions.rst @@ -5,7 +5,7 @@ .. rst-class:: hidden .. math:: - \gdef\dirac#1{\operatorname{\delta}\left(#1\right)} + \gdef\dirac#1{\mathop{{}\delta}\left(#1\right)} \gdef\e#1{\operatorname{e}^{#1}} \gdef\Hankel#1#2#3{\mathop{{}H_{#2}^{(#1)}}\!\left(#3\right)} \gdef\hankel#1#2#3{\mathop{{}h_{#2}^{(#1)}}\!\left(#3\right)} From 33eab328cdd9be877f9a3767f8d776194b9dab36 Mon Sep 17 00:00:00 2001 From: Hagen Wierstorf Date: Mon, 18 Mar 2019 10:55:17 +0100 Subject: [PATCH 074/101] Hide warnings for a few undefined points --- sfs/fd/source.py | 9 +++++++-- sfs/td/source.py | 10 +++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/sfs/fd/source.py b/sfs/fd/source.py index 993bcba..3a32b86 100644 --- a/sfs/fd/source.py +++ b/sfs/fd/source.py @@ -82,7 +82,10 @@ def point(omega, x0, grid, *, c=None): grid = util.as_xyz_components(grid) r = np.linalg.norm(grid - x0) - return 1 / (4*np.pi) * np.exp(-1j * k * r) / r + # If r is 0, the sound pressure is complex infinity + numerator = np.exp(-1j * k * r) / (4*np.pi) + with np.errstate(invalid='ignore', divide='ignore'): + return numerator / r def point_velocity(omega, x0, grid, *, c=None, rho0=None): @@ -395,7 +398,9 @@ def point_image_sources(omega, x0, grid, L, *, max_order, coeffs=None, c=None): p = 0 for position, strength in zip(xs, source_strengths): if strength != 0: - p += strength * point(omega, position, grid, c=c) + # point can be complex infinity + with np.errstate(invalid='ignore'): + p += strength * point(omega, position, grid, c=c) return p diff --git a/sfs/td/source.py b/sfs/td/source.py index bf9e9aa..017aeb4 100644 --- a/sfs/td/source.py +++ b/sfs/td/source.py @@ -79,13 +79,17 @@ def point(xs, signal, observation_time, grid, c=None): if c is None: c = default.c r = np.linalg.norm(grid - xs) - # evaluate g over grid - weights = 1 / (4 * np.pi * r) + # If r is +-0, the sound pressure is +-infinity + with np.errstate(divide='ignore'): + weights = 1 / (4 * np.pi * r) delays = r / c base_time = observation_time - signal_offset - return weights * np.interp(base_time - delays, + points_at_time = np.interp(base_time - delays, np.arange(len(data)) / samplerate, data, left=0, right=0) + # weights can be +-infinity + with np.errstate(invalid='ignore'): + return weights * points_at_time def point_image_sources(x0, signal, observation_time, grid, L, max_order, From 79e317d2c6681d0ec638d965eac5e0490cea0ce4 Mon Sep 17 00:00:00 2001 From: Hagen Wierstorf Date: Mon, 18 Mar 2019 11:15:55 +0100 Subject: [PATCH 075/101] Release 0.5.0 --- NEWS.rst | 14 ++++++++++++++ sfs/__init__.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index 94ca333..6df799c 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,6 +1,20 @@ Version History =============== +Version 0.5.0 (2019-03-18): + * Switching to separate `sfs.plot2d` and `sfs.plot3d` for plotting functions + * Move `sfs.util.displacement()` to `sfs.fd.displacement()` + * Switch to keyword only arguments + * New default driving function for `sfs.fd.wfs.point_25d()` + * New driving function syntax, e.g. `sfs.fd.wfs.point_25d()` + * Example for the sound field of a pulsating sphere + * Add time domain NFC-HOA driving functions `sfs.td.nfchoa` + * `sfs.fd.synthesize()`, `sfs.td.synthesize()` for soundfield superposition + * Change `sfs.mono` to `sfs.fd` and `sfs.time` to `sfs.td` + * Move source selection helpers to `sfs.util` + * Use `sfs.default` object instead of `sfs.defs` submodule + * Drop support for legacy Python 2.7 + Version 0.4.0 (2018-03-14): * Driving functions in time domain for a plane wave, point source, and focused source diff --git a/sfs/__init__.py b/sfs/__init__.py index a1c356f..2072d81 100644 --- a/sfs/__init__.py +++ b/sfs/__init__.py @@ -16,7 +16,7 @@ util """ -__version__ = "0.4.0" +__version__ = "0.5.0" class default: From 14d6dace91f4a8567b4e9cd6812bf8953a3e03b4 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sun, 17 Mar 2019 15:30:03 +0100 Subject: [PATCH 076/101] Extend _register_cmap_clip() --- sfs/plot2d.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sfs/plot2d.py b/sfs/plot2d.py index 9efba27..0bd86c7 100644 --- a/sfs/plot2d.py +++ b/sfs/plot2d.py @@ -13,8 +13,11 @@ def _register_cmap_clip(name, original_cmap, alpha): """Create a color map with "over" and "under" values.""" from matplotlib.colors import LinearSegmentedColormap - cdict = plt.cm.datad[original_cmap] - cmap = LinearSegmentedColormap(name, cdict) + cdata = plt.cm.datad[original_cmap] + if isinstance(cdata, dict): + cmap = LinearSegmentedColormap(name, cdata) + else: + cmap = LinearSegmentedColormap.from_list(name, cdata) cmap.set_over([alpha * c + 1 - alpha for c in cmap(1.0)[:3]]) cmap.set_under([alpha * c + 1 - alpha for c in cmap(0.0)[:3]]) plt.cm.register_cmap(cmap=cmap) From ddbe35d3f6128e860a1fc8b3b2a7b0e857015064 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 16 Mar 2019 14:37:16 +0100 Subject: [PATCH 077/101] Avoid namespace pollution ... by prefixing imported names with an underscore. --- sfs/array.py | 140 +++++++++++------------ sfs/fd/__init__.py | 3 +- sfs/fd/esa.py | 166 ++++++++++++++-------------- sfs/fd/nfchoa.py | 70 ++++++------ sfs/fd/sdm.py | 77 ++++++------- sfs/fd/source.py | 270 +++++++++++++++++++++++---------------------- sfs/fd/wfs.py | 205 +++++++++++++++++----------------- sfs/plot2d.py | 140 +++++++++++------------ sfs/plot3d.py | 11 +- sfs/tapering.py | 26 ++--- sfs/td/__init__.py | 9 +- sfs/td/nfchoa.py | 143 ++++++++++++------------ sfs/td/source.py | 34 +++--- sfs/td/wfs.py | 82 +++++++------- 14 files changed, 696 insertions(+), 680 deletions(-) diff --git a/sfs/array.py b/sfs/array.py index 045b8d8..899075c 100644 --- a/sfs/array.py +++ b/sfs/array.py @@ -9,13 +9,15 @@ plt.rcParams['axes.grid'] = True """ -from collections import namedtuple -import numpy as np -from . import util +from collections import namedtuple as _namedtuple +import numpy as _np -class SecondarySourceDistribution(namedtuple('SecondarySourceDistribution', - 'x n a')): +from . import util as _util + + +class SecondarySourceDistribution(_namedtuple('SecondarySourceDistribution', + 'x n a')): """Named tuple returned by array functions. See `collections.namedtuple`. @@ -70,17 +72,17 @@ def as_secondary_source_distribution(arg, **kwargs): a = 1.0 elif len(arg) == 1: x, = arg - n = np.nan, np.nan, np.nan + n = _np.nan, _np.nan, _np.nan a = 1.0 else: raise TypeError('Between 1 and 3 elements are required') - x = util.asarray_of_rows(x, **kwargs) - n = util.asarray_of_rows(n, **kwargs) + x = _util.asarray_of_rows(x, **kwargs) + n = _util.asarray_of_rows(n, **kwargs) if len(n) == 1: - n = np.tile(n, (len(x), 1)) - a = util.asarray_1d(a, **kwargs) + n = _np.tile(n, (len(x), 1)) + a = _util.asarray_1d(a, **kwargs) if len(a) == 1: - a = np.tile(a, len(x)) + a = _np.tile(a, len(x)) return SecondarySourceDistribution(x, n, a) @@ -116,7 +118,7 @@ def linear(N, spacing, *, center=[0, 0, 0], orientation=[1, 0, 0]): plt.ylabel('y / m') """ - return _linear_helper(np.arange(N) * spacing, center, orientation) + return _linear_helper(_np.arange(N) * spacing, center, orientation) def linear_diff(distances, *, center=[0, 0, 0], orientation=[1, 0, 0]): @@ -147,8 +149,8 @@ def linear_diff(distances, *, center=[0, 0, 0], orientation=[1, 0, 0]): plt.ylabel('y / m') """ - distances = util.asarray_1d(distances) - ycoordinates = np.concatenate(([0], np.cumsum(distances))) + distances = _util.asarray_1d(distances) + ycoordinates = _np.concatenate(([0], _np.cumsum(distances))) return _linear_helper(ycoordinates, center, orientation) @@ -188,7 +190,7 @@ def linear_random(N, min_spacing, max_spacing, *, center=[0, 0, 0], plt.ylabel('y / m') """ - r = np.random.RandomState(seed) + r = _np.random.RandomState(seed) distances = r.uniform(min_spacing, max_spacing, size=N-1) return linear_diff(distances, center=center, orientation=orientation) @@ -222,16 +224,16 @@ def circular(N, R, *, center=[0, 0, 0]): plt.ylabel('y / m') """ - center = util.asarray_1d(center) - alpha = np.linspace(0, 2 * np.pi, N, endpoint=False) - positions = np.zeros((N, len(center))) - positions[:, 0] = R * np.cos(alpha) - positions[:, 1] = R * np.sin(alpha) + center = _util.asarray_1d(center) + alpha = _np.linspace(0, 2 * _np.pi, N, endpoint=False) + positions = _np.zeros((N, len(center))) + positions[:, 0] = R * _np.cos(alpha) + positions[:, 1] = R * _np.sin(alpha) positions += center - normals = np.zeros_like(positions) - normals[:, 0] = np.cos(alpha + np.pi) - normals[:, 1] = np.sin(alpha + np.pi) - weights = np.ones(N) * 2 * np.pi * R / N + normals = _np.zeros_like(positions) + normals[:, 0] = _np.cos(alpha + _np.pi) + normals[:, 1] = _np.sin(alpha + _np.pi) + weights = _np.ones(N) * 2 * _np.pi * R / N return SecondarySourceDistribution(positions, normals, weights) @@ -268,9 +270,9 @@ def rectangular(N, spacing, *, center=[0, 0, 0], orientation=[1, 0, 0]): plt.ylabel('y / m') """ - N1, N2 = (N, N) if np.isscalar(N) else N - offset1 = spacing * (N2 - 1) / 2 + spacing / np.sqrt(2) - offset2 = spacing * (N1 - 1) / 2 + spacing / np.sqrt(2) + N1, N2 = (N, N) if _np.isscalar(N) else N + offset1 = spacing * (N2 - 1) / 2 + spacing / _np.sqrt(2) + offset2 = spacing * (N1 - 1) / 2 + spacing / _np.sqrt(2) positions, normals, weights = concatenate( # left linear(N1, spacing, center=[-offset1, 0, 0], orientation=[1, 0, 0]), @@ -321,37 +323,37 @@ def rounded_edge(Nxy, Nr, dx, *, center=[0, 0, 0], orientation=[1, 0, 0]): """ # radius of rounded edge Nr += 1 - R = 2/np.pi * Nr * dx + R = 2/_np.pi * Nr * dx # array along y-axis x00, n00, a00 = linear(Nxy, dx, center=[0, Nxy//2*dx+dx/2+R, 0]) - x00 = np.flipud(x00) + x00 = _np.flipud(x00) positions = x00 directions = n00 weights = a00 # round part - x00 = np.zeros((Nr, 3)) - n00 = np.zeros((Nr, 3)) - a00 = np.zeros(Nr) + x00 = _np.zeros((Nr, 3)) + n00 = _np.zeros((Nr, 3)) + a00 = _np.zeros(Nr) for n in range(0, Nr): - alpha = np.pi/2 * n/Nr - x00[n, 0] = R * (1-np.cos(alpha)) - x00[n, 1] = R * (1-np.sin(alpha)) - n00[n, 0] = np.cos(alpha) - n00[n, 1] = np.sin(alpha) + alpha = _np.pi/2 * n/Nr + x00[n, 0] = R * (1 - _np.cos(alpha)) + x00[n, 1] = R * (1 - _np.sin(alpha)) + n00[n, 0] = _np.cos(alpha) + n00[n, 1] = _np.sin(alpha) a00[n] = dx - positions = np.concatenate((positions, x00)) - directions = np.concatenate((directions, n00)) - weights = np.concatenate((weights, a00)) + positions = _np.concatenate((positions, x00)) + directions = _np.concatenate((directions, n00)) + weights = _np.concatenate((weights, a00)) # array along x-axis x00, n00, a00 = linear(Nxy, dx, center=[Nxy//2*dx-dx/2+R, 0, 0], orientation=[0, 1, 0]) - x00 = np.flipud(x00) - positions = np.concatenate((positions, x00)) - directions = np.concatenate((directions, n00)) - weights = np.concatenate((weights, a00)) + x00 = _np.flipud(x00) + positions = _np.concatenate((positions, x00)) + directions = _np.concatenate((directions, n00)) + weights = _np.concatenate((weights, a00)) # rotate array positions, directions = _rotate_array(positions, directions, @@ -392,7 +394,7 @@ def edge(Nxy, dx, *, center=[0, 0, 0], orientation=[1, 0, 0]): """ # array along y-axis x00, n00, a00 = linear(Nxy, dx, center=[0, Nxy//2*dx+dx/2, 0]) - x00 = np.flipud(x00) + x00 = _np.flipud(x00) positions = x00 directions = n00 weights = a00 @@ -400,10 +402,10 @@ def edge(Nxy, dx, *, center=[0, 0, 0], orientation=[1, 0, 0]): # array along x-axis x00, n00, a00 = linear(Nxy, dx, center=[Nxy//2*dx-dx/2, 0, 0], orientation=[0, 1, 0]) - x00 = np.flipud(x00) - positions = np.concatenate((positions, x00)) - directions = np.concatenate((directions, n00)) - weights = np.concatenate((weights, a00)) + x00 = _np.flipud(x00) + positions = _np.concatenate((positions, x00)) + directions = _np.concatenate((directions, n00)) + weights = _np.concatenate((weights, a00)) # rotate array positions, directions = _rotate_array(positions, directions, @@ -452,9 +454,9 @@ def planar(N, spacing, *, center=[0, 0, 0], orientation=[1, 0, 0]): """ - N1, N2 = (N, N) if np.isscalar(N) else N - zcoordinates = np.arange(N2) * spacing - zcoordinates -= np.mean(zcoordinates[[0, -1]]) # move center to origin + N1, N2 = (N, N) if _np.isscalar(N) else N + zcoordinates = _np.arange(N2) * spacing + zcoordinates -= _np.mean(zcoordinates[[0, -1]]) # move center to origin subarrays = [linear(N1, spacing, center=[0, 0, z]) for z in zcoordinates] positions, normals, weights = concatenate(*subarrays) weights *= spacing @@ -499,11 +501,11 @@ def cube(N, spacing, *, center=[0, 0, 0], orientation=[1, 0, 0]): plt.title('view onto xy-plane') """ - N1, N2, N3 = (N, N, N) if np.isscalar(N) else N + N1, N2, N3 = (N, N, N) if _np.isscalar(N) else N d = spacing - offset1 = d * (N2 - 1) / 2 + d / np.sqrt(2) - offset2 = d * (N1 - 1) / 2 + d / np.sqrt(2) - offset3 = d * (N3 - 1) / 2 + d / np.sqrt(2) + offset1 = d * (N2 - 1) / 2 + d / _np.sqrt(2) + offset2 = d * (N1 - 1) / 2 + d / _np.sqrt(2) + offset3 = d * (N3 - 1) / 2 + d / _np.sqrt(2) positions, directions, weights = concatenate( # west planar((N1, N3), d, center=[-offset1, 0, 0], orientation=[1, 0, 0]), @@ -565,7 +567,7 @@ def sphere_load(file, radius, *, center=[0, 0, 0]): plt.title('view onto xy-plane') """ - data = np.loadtxt(file) + data = _np.loadtxt(file) positions, weights = data[:, :3], data[:, 3] normals = -positions positions *= radius @@ -618,7 +620,7 @@ def load(file, *, center=[0, 0, 0], orientation=[1, 0, 0]): plt.title('top view of 64 channel WFS system at university of Rostock') """ - data = np.loadtxt(file, 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) @@ -657,34 +659,34 @@ def weights_midpoint(positions, *, closed): 0.0003152601902411123 """ - positions = util.asarray_of_rows(positions) + positions = _util.asarray_of_rows(positions) if closed: before, after = -1, 0 # cyclic else: before, after = 1, -2 # mirrored - positions = np.row_stack((positions[before], positions, positions[after])) - distances = np.linalg.norm(np.diff(positions, axis=0), axis=1) + positions = _np.row_stack((positions[before], positions, positions[after])) + distances = _np.linalg.norm(_np.diff(positions, axis=0), axis=1) return (distances[:-1] + distances[1:]) / 2 def _rotate_array(positions, normals, n1, n2): """Rotate secondary sources from n1 to n2.""" - R = util.rotation_matrix(n1, n2) - positions = np.inner(positions, R) - normals = np.inner(normals, R) + R = _util.rotation_matrix(n1, n2) + positions = _np.inner(positions, R) + normals = _np.inner(normals, R) return positions, normals def _linear_helper(ycoordinates, center, orientation): """Create a full linear array from an array of y-coordinates.""" - center = util.asarray_1d(center) + center = _util.asarray_1d(center) N = len(ycoordinates) - positions = np.zeros((N, 3)) - positions[:, 1] = ycoordinates - np.mean(ycoordinates[[0, -1]]) + positions = _np.zeros((N, 3)) + positions[:, 1] = ycoordinates - _np.mean(ycoordinates[[0, -1]]) positions, normals = _rotate_array(positions, [1, 0, 0], [1, 0, 0], orientation) positions += center - normals = np.tile(normals, (N, 1)) + normals = _np.tile(normals, (N, 1)) weights = weights_midpoint(positions, closed=False) return SecondarySourceDistribution(positions, normals, weights) @@ -712,5 +714,5 @@ def concatenate(*arrays): plt.ylabel('y / m') """ - return SecondarySourceDistribution._make(np.concatenate(i) + return SecondarySourceDistribution._make(_np.concatenate(i) for i in zip(*arrays)) diff --git a/sfs/fd/__init__.py b/sfs/fd/__init__.py index 89b9e1e..2d9555e 100644 --- a/sfs/fd/__init__.py +++ b/sfs/fd/__init__.py @@ -11,10 +11,11 @@ esa """ +import numpy as _np + from . import source from .. import array as _array from .. import util as _util -import numpy as _np def shiftphase(p, phase): diff --git a/sfs/fd/esa.py b/sfs/fd/esa.py index 583811e..380769c 100644 --- a/sfs/fd/esa.py +++ b/sfs/fd/esa.py @@ -9,14 +9,16 @@ to ESA in their specific geometries (spherical/circular, planar/linear). """ +import numpy as _np +from scipy.special import jn as _jn, hankel2 as _hankel2 -import numpy as np -from scipy.special import jn, hankel2 -from .. import util -from . import secondary_source_line, secondary_source_point +from . import secondary_source_line as _secondary_source_line +from . import secondary_source_point as _secondary_source_point +from .. import util as _util -def plane_2d_edge(omega, x0, n=[0, 1, 0], *, alpha=3/2*np.pi, Nc=None, c=None): +def plane_2d_edge(omega, x0, n=[0, 1, 0], *, alpha=_np.pi*3/2, Nc=None, + c=None): r"""Driving function for 2-dimensional plane wave with edge ESA. Driving function for a virtual plane wave using the 2-dimensional ESA @@ -58,35 +60,35 @@ def plane_2d_edge(omega, x0, n=[0, 1, 0], *, alpha=3/2*np.pi, Nc=None, c=None): Derived from :cite:`Spors2016` """ - x0 = np.asarray(x0) - n = util.normalize_vector(n) - k = util.wavenumber(omega, c) - phi_s = np.arctan2(n[1], n[0]) + np.pi + x0 = _np.asarray(x0) + n = _util.normalize_vector(n) + k = _util.wavenumber(omega, c) + phi_s = _np.arctan2(n[1], n[0]) + _np.pi L = x0.shape[0] - r = np.linalg.norm(x0, axis=1) - phi = np.arctan2(x0[:, 1], x0[:, 0]) - phi = np.where(phi < 0, phi+2*np.pi, phi) + r = _np.linalg.norm(x0, axis=1) + phi = _np.arctan2(x0[:, 1], x0[:, 0]) + phi = _np.where(phi < 0, phi + 2 * _np.pi, phi) if Nc is None: - Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) + Nc = _np.ceil(2 * k * _np.max(r) * alpha / _np.pi) - epsilon = np.ones(Nc) # weights for series expansion + epsilon = _np.ones(Nc) # weights for series expansion epsilon[0] = 2 - d = np.zeros(L, dtype=complex) - for m in np.arange(Nc): - nu = m*np.pi/alpha - d = d + 1/epsilon[m] * np.exp(1j*nu*np.pi/2) * np.sin(nu*phi_s) \ - * np.cos(nu*phi) * nu/r * jn(nu, k*r) + d = _np.zeros(L, dtype=complex) + for m in _np.arange(Nc): + nu = m * _np.pi / alpha + d = d + 1/epsilon[m] * _np.exp(1j*nu*_np.pi/2) * _np.sin(nu*phi_s) \ + * _np.cos(nu*phi) * nu/r * _jn(nu, k*r) d[phi > 0] = -d[phi > 0] - selection = util.source_selection_all(len(x0)) - return 4*np.pi/alpha * d, selection, secondary_source_line(omega, c) + selection = _util.source_selection_all(len(x0)) + return 4*_np.pi/alpha * d, selection, _secondary_source_line(omega, c) -def plane_2d_edge_dipole_ssd(omega, x0, n=[0, 1, 0], *, alpha=3/2*np.pi, +def plane_2d_edge_dipole_ssd(omega, x0, n=[0, 1, 0], *, alpha=_np.pi*3/2, Nc=None, c=None): r"""Driving function for 2-dimensional plane wave with edge dipole ESA. @@ -129,32 +131,32 @@ def plane_2d_edge_dipole_ssd(omega, x0, n=[0, 1, 0], *, alpha=3/2*np.pi, Derived from :cite:`Spors2016` """ - x0 = np.asarray(x0) - n = util.normalize_vector(n) - k = util.wavenumber(omega, c) - phi_s = np.arctan2(n[1], n[0]) + np.pi + x0 = _np.asarray(x0) + n = _util.normalize_vector(n) + k = _util.wavenumber(omega, c) + phi_s = _np.arctan2(n[1], n[0]) + _np.pi L = x0.shape[0] - r = np.linalg.norm(x0, axis=1) - phi = np.arctan2(x0[:, 1], x0[:, 0]) - phi = np.where(phi < 0, phi+2*np.pi, phi) + r = _np.linalg.norm(x0, axis=1) + phi = _np.arctan2(x0[:, 1], x0[:, 0]) + phi = _np.where(phi < 0, phi + 2 * _np.pi, phi) if Nc is None: - Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) + Nc = _np.ceil(2 * k * _np.max(r) * alpha / _np.pi) - epsilon = np.ones(Nc) # weights for series expansion + epsilon = _np.ones(Nc) # weights for series expansion epsilon[0] = 2 - d = np.zeros(L, dtype=complex) - for m in np.arange(Nc): - nu = m*np.pi/alpha - d = d + 1/epsilon[m] * np.exp(1j*nu*np.pi/2) * np.cos(nu*phi_s) \ - * np.cos(nu*phi) * jn(nu, k*r) + d = _np.zeros(L, dtype=complex) + for m in _np.arange(Nc): + nu = m * _np.pi / alpha + d = d + 1/epsilon[m] * _np.exp(1j*nu*_np.pi/2) * _np.cos(nu*phi_s) \ + * _np.cos(nu*phi) * _jn(nu, k*r) - return 4*np.pi/alpha * d + return 4*_np.pi/alpha * d -def line_2d_edge(omega, x0, xs, *, alpha=3/2*np.pi, Nc=None, c=None): +def line_2d_edge(omega, x0, xs, *, alpha=_np.pi*3/2, Nc=None, c=None): r"""Driving function for 2-dimensional line source with edge ESA. Driving function for a virtual line source using the 2-dimensional ESA @@ -196,39 +198,39 @@ def line_2d_edge(omega, x0, xs, *, alpha=3/2*np.pi, Nc=None, c=None): Derived from :cite:`Spors2016` """ - x0 = np.asarray(x0) - k = util.wavenumber(omega, c) - phi_s = np.arctan2(xs[1], xs[0]) + x0 = _np.asarray(x0) + k = _util.wavenumber(omega, c) + phi_s = _np.arctan2(xs[1], xs[0]) if phi_s < 0: - phi_s = phi_s + 2*np.pi - r_s = np.linalg.norm(xs) + phi_s = phi_s + 2 * _np.pi + r_s = _np.linalg.norm(xs) L = x0.shape[0] - r = np.linalg.norm(x0, axis=1) - phi = np.arctan2(x0[:, 1], x0[:, 0]) - phi = np.where(phi < 0, phi+2*np.pi, phi) + r = _np.linalg.norm(x0, axis=1) + phi = _np.arctan2(x0[:, 1], x0[:, 0]) + phi = _np.where(phi < 0, phi + 2 * _np.pi, phi) if Nc is None: - Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) + Nc = _np.ceil(2 * k * _np.max(r) * alpha / _np.pi) - epsilon = np.ones(Nc) # weights for series expansion + epsilon = _np.ones(Nc) # weights for series expansion epsilon[0] = 2 - d = np.zeros(L, dtype=complex) + d = _np.zeros(L, dtype=complex) idx = (r <= r_s) - for m in np.arange(Nc): - nu = m*np.pi/alpha - f = 1/epsilon[m] * np.sin(nu*phi_s) * np.cos(nu*phi) * nu/r - d[idx] = d[idx] + f[idx] * jn(nu, k*r[idx]) * hankel2(nu, k*r_s) - d[~idx] = d[~idx] + f[~idx] * jn(nu, k*r_s) * hankel2(nu, k*r[~idx]) + for m in _np.arange(Nc): + nu = m * _np.pi / alpha + f = 1/epsilon[m] * _np.sin(nu*phi_s) * _np.cos(nu*phi) * nu/r + d[idx] = d[idx] + f[idx] * _jn(nu, k*r[idx]) * _hankel2(nu, k*r_s) + d[~idx] = d[~idx] + f[~idx] * _jn(nu, k*r_s) * _hankel2(nu, k*r[~idx]) d[phi > 0] = -d[phi > 0] - selection = util.source_selection_all(len(x0)) - return -1j*np.pi/alpha * d, selection, secondary_source_line(omega, c) + selection = _util.source_selection_all(len(x0)) + return -1j*_np.pi/alpha * d, selection, _secondary_source_line(omega, c) -def line_2d_edge_dipole_ssd(omega, x0, xs, *, alpha=3/2*np.pi, Nc=None, +def line_2d_edge_dipole_ssd(omega, x0, xs, *, alpha=_np.pi*3/2, Nc=None, c=None): r"""Driving function for 2-dimensional line source with edge dipole ESA. @@ -271,36 +273,36 @@ def line_2d_edge_dipole_ssd(omega, x0, xs, *, alpha=3/2*np.pi, Nc=None, Derived from :cite:`Spors2016` """ - x0 = np.asarray(x0) - k = util.wavenumber(omega, c) - phi_s = np.arctan2(xs[1], xs[0]) + x0 = _np.asarray(x0) + k = _util.wavenumber(omega, c) + phi_s = _np.arctan2(xs[1], xs[0]) if phi_s < 0: - phi_s = phi_s + 2*np.pi - r_s = np.linalg.norm(xs) + phi_s = phi_s + 2 * _np.pi + r_s = _np.linalg.norm(xs) L = x0.shape[0] - r = np.linalg.norm(x0, axis=1) - phi = np.arctan2(x0[:, 1], x0[:, 0]) - phi = np.where(phi < 0, phi+2*np.pi, phi) + r = _np.linalg.norm(x0, axis=1) + phi = _np.arctan2(x0[:, 1], x0[:, 0]) + phi = _np.where(phi < 0, phi + 2 * _np.pi, phi) if Nc is None: - Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) + Nc = _np.ceil(2 * k * _np.max(r) * alpha / _np.pi) - epsilon = np.ones(Nc) # weights for series expansion + epsilon = _np.ones(Nc) # weights for series expansion epsilon[0] = 2 - d = np.zeros(L, dtype=complex) + d = _np.zeros(L, dtype=complex) idx = (r <= r_s) - for m in np.arange(Nc): - nu = m*np.pi/alpha - f = 1/epsilon[m] * np.cos(nu*phi_s) * np.cos(nu*phi) - d[idx] = d[idx] + f[idx] * jn(nu, k*r[idx]) * hankel2(nu, k*r_s) - d[~idx] = d[~idx] + f[~idx] * jn(nu, k*r_s) * hankel2(nu, k*r[~idx]) + for m in _np.arange(Nc): + nu = m * _np.pi / alpha + f = 1/epsilon[m] * _np.cos(nu*phi_s) * _np.cos(nu*phi) + d[idx] = d[idx] + f[idx] * _jn(nu, k*r[idx]) * _hankel2(nu, k*r_s) + d[~idx] = d[~idx] + f[~idx] * _jn(nu, k*r_s) * _hankel2(nu, k*r[~idx]) - return -1j*np.pi/alpha * d + return -1j*_np.pi/alpha * d -def point_25d_edge(omega, x0, xs, *, xref=[2, -2, 0], alpha=3/2*np.pi, +def point_25d_edge(omega, x0, xs, *, xref=[2, -2, 0], alpha=_np.pi*3/2, Nc=None, c=None): r"""Driving function for 2.5-dimensional point source with edge ESA. @@ -345,14 +347,14 @@ def point_25d_edge(omega, x0, xs, *, xref=[2, -2, 0], alpha=3/2*np.pi, Derived from :cite:`Spors2016` """ - x0 = np.asarray(x0) - xs = np.asarray(xs) - xref = np.asarray(xref) + x0 = _np.asarray(x0) + xs = _np.asarray(xs) + xref = _np.asarray(xref) - if np.isscalar(xref): - a = np.linalg.norm(xref)/np.linalg.norm(xref-xs) + if _np.isscalar(xref): + a = _np.linalg.norm(xref) / _np.linalg.norm(xref - xs) else: - a = np.linalg.norm(xref-x0, axis=1)/np.linalg.norm(xref-xs) + a = _np.linalg.norm(xref - x0, axis=1) / _np.linalg.norm(xref - xs) d, selection, _ = line_2d_edge(omega, x0, xs, alpha=alpha, Nc=Nc, c=c) - return 1j*np.sqrt(a) * d, selection, secondary_source_point(omega, c) + return 1j*_np.sqrt(a) * d, selection, _secondary_source_point(omega, c) diff --git a/sfs/fd/nfchoa.py b/sfs/fd/nfchoa.py index 0530034..e326a43 100644 --- a/sfs/fd/nfchoa.py +++ b/sfs/fd/nfchoa.py @@ -28,11 +28,11 @@ def plot(d, selection, secondary_source): sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15) """ +import numpy as _np +from scipy.special import hankel2 as _hankel2 -import numpy as np -from scipy.special import hankel2 -from .. import util -from . import secondary_source_point +from . import secondary_source_point as _secondary_source_point +from .. import util as _util def plane_2d(omega, x0, r0, n=[0, 1, 0], *, max_order=None, c=None): @@ -87,18 +87,18 @@ def plane_2d(omega, x0, r0, n=[0, 1, 0], *, max_order=None, c=None): """ if max_order is None: - max_order = util.max_order_circular_harmonics(len(x0)) + max_order = _util.max_order_circular_harmonics(len(x0)) - x0 = util.asarray_of_rows(x0) - k = util.wavenumber(omega, c) - n = util.normalize_vector(n) - phi, _, r = util.cart2sph(*n) - phi0 = util.cart2sph(*x0.T)[0] + x0 = _util.asarray_of_rows(x0) + k = _util.wavenumber(omega, c) + n = _util.normalize_vector(n) + phi, _, r = _util.cart2sph(*n) + phi0 = _util.cart2sph(*x0.T)[0] d = 0 for m in range(-max_order, max_order + 1): - d += 1j**-m / hankel2(m, k * r0) * np.exp(1j * m * (phi0 - phi)) - selection = util.source_selection_all(len(x0)) - return -2j / (np.pi*r0) * d, selection, secondary_source_point(omega, c) + d += 1j**-m / _hankel2(m, k * r0) * _np.exp(1j * m * (phi0 - phi)) + selection = _util.source_selection_all(len(x0)) + return -2j / (_np.pi*r0) * d, selection, _secondary_source_point(omega, c) def point_25d(omega, x0, r0, xs, *, max_order=None, c=None): @@ -153,20 +153,20 @@ def point_25d(omega, x0, r0, xs, *, max_order=None, c=None): """ if max_order is None: - max_order = util.max_order_circular_harmonics(len(x0)) - - x0 = util.asarray_of_rows(x0) - k = util.wavenumber(omega, c) - xs = util.asarray_1d(xs) - phi, _, r = util.cart2sph(*xs) - phi0 = util.cart2sph(*x0.T)[0] - hr = util.spherical_hn2(range(0, max_order + 1), k * r) - hr0 = util.spherical_hn2(range(0, max_order + 1), k * r0) + max_order = _util.max_order_circular_harmonics(len(x0)) + + x0 = _util.asarray_of_rows(x0) + k = _util.wavenumber(omega, c) + xs = _util.asarray_1d(xs) + phi, _, r = _util.cart2sph(*xs) + phi0 = _util.cart2sph(*x0.T)[0] + hr = _util.spherical_hn2(range(0, max_order + 1), k * r) + hr0 = _util.spherical_hn2(range(0, max_order + 1), k * r0) d = 0 for m in range(-max_order, max_order + 1): - d += hr[abs(m)] / hr0[abs(m)] * np.exp(1j * m * (phi0 - phi)) - selection = util.source_selection_all(len(x0)) - return d / (2 * np.pi * r0), selection, secondary_source_point(omega, c) + d += hr[abs(m)] / hr0[abs(m)] * _np.exp(1j * m * (phi0 - phi)) + selection = _util.source_selection_all(len(x0)) + return d / (2 * _np.pi * r0), selection, _secondary_source_point(omega, c) def plane_25d(omega, x0, r0, n=[0, 1, 0], *, max_order=None, c=None): @@ -221,16 +221,16 @@ def plane_25d(omega, x0, r0, n=[0, 1, 0], *, max_order=None, c=None): """ if max_order is None: - max_order = util.max_order_circular_harmonics(len(x0)) + max_order = _util.max_order_circular_harmonics(len(x0)) - x0 = util.asarray_of_rows(x0) - k = util.wavenumber(omega, c) - n = util.normalize_vector(n) - phi, _, r = util.cart2sph(*n) - phi0 = util.cart2sph(*x0.T)[0] + x0 = _util.asarray_of_rows(x0) + k = _util.wavenumber(omega, c) + n = _util.normalize_vector(n) + phi, _, r = _util.cart2sph(*n) + phi0 = _util.cart2sph(*x0.T)[0] d = 0 - hn2 = util.spherical_hn2(range(0, max_order + 1), k * r0) + hn2 = _util.spherical_hn2(range(0, max_order + 1), k * r0) for m in range(-max_order, max_order + 1): - d += (-1j)**abs(m) / (k * hn2[abs(m)]) * np.exp(1j * m * (phi0 - phi)) - selection = util.source_selection_all(len(x0)) - return 2*1j / r0 * d, selection, secondary_source_point(omega, c) + d += (-1j)**abs(m) / (k * hn2[abs(m)]) * _np.exp(1j * m * (phi0 - phi)) + selection = _util.source_selection_all(len(x0)) + return 2*1j / r0 * d, selection, _secondary_source_point(omega, c) diff --git a/sfs/fd/sdm.py b/sfs/fd/sdm.py index fc9a583..7682e3d 100644 --- a/sfs/fd/sdm.py +++ b/sfs/fd/sdm.py @@ -27,11 +27,12 @@ def plot(d, selection, secondary_source): sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15) """ +import numpy as _np +from scipy.special import hankel2 as _hankel2 -import numpy as np -from scipy.special import hankel2 -from .. import util -from . import secondary_source_line, secondary_source_point +from . import secondary_source_line as _secondary_source_line +from . import secondary_source_point as _secondary_source_point +from .. import util as _util def line_2d(omega, x0, n0, xs, *, c=None): @@ -76,15 +77,15 @@ def line_2d(omega, x0, n0, xs, *, c=None): plot(d, selection, secondary_source) """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - k = util.wavenumber(omega, c) + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + xs = _util.asarray_1d(xs) + k = _util.wavenumber(omega, c) ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - d = - 1j/2 * k * xs[1] / r * hankel2(1, k * r) - selection = util.source_selection_all(len(x0)) - return d, selection, secondary_source_line(omega, c) + r = _np.linalg.norm(ds, axis=1) + d = - 1j/2 * k * xs[1] / r * _hankel2(1, k * r) + selection = _util.source_selection_all(len(x0)) + return d, selection, _secondary_source_line(omega, c) def plane_2d(omega, x0, n0, n=[0, 1, 0], *, c=None): @@ -133,13 +134,13 @@ def plane_2d(omega, x0, n0, n=[0, 1, 0], *, c=None): plot(d, selection, secondary_source) """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - n = util.normalize_vector(n) - k = util.wavenumber(omega, c) - d = k * n[1] * np.exp(-1j * k * n[0] * x0[:, 0]) - selection = util.source_selection_all(len(x0)) - return d, selection, secondary_source_line(omega, c) + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + n = _util.normalize_vector(n) + k = _util.wavenumber(omega, c) + d = k * n[1] * _np.exp(-1j * k * n[0] * x0[:, 0]) + selection = _util.source_selection_all(len(x0)) + return d, selection, _secondary_source_line(omega, c) def plane_25d(omega, x0, n0, n=[0, 1, 0], *, xref=[0, 0, 0], c=None): @@ -186,15 +187,15 @@ def plane_25d(omega, x0, n0, n=[0, 1, 0], *, xref=[0, 0, 0], c=None): plot(d, selection, secondary_source) """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - n = util.normalize_vector(n) - xref = util.asarray_1d(xref) - k = util.wavenumber(omega, c) - d = 4j * np.exp(-1j*k*n[1]*xref[1]) / hankel2(0, k*n[1]*xref[1]) * \ - np.exp(-1j*k*n[0]*x0[:, 0]) - selection = util.source_selection_all(len(x0)) - return d, selection, secondary_source_point(omega, c) + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + n = _util.normalize_vector(n) + xref = _util.asarray_1d(xref) + k = _util.wavenumber(omega, c) + d = 4j * _np.exp(-1j*k*n[1]*xref[1]) / _hankel2(0, k*n[1]*xref[1]) * \ + _np.exp(-1j*k*n[0]*x0[:, 0]) + selection = _util.source_selection_all(len(x0)) + return d, selection, _secondary_source_point(omega, c) def point_25d(omega, x0, n0, xs, *, xref=[0, 0, 0], c=None): @@ -241,14 +242,14 @@ def point_25d(omega, x0, n0, xs, *, xref=[0, 0, 0], c=None): plot(d, selection, secondary_source) """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - xref = util.asarray_1d(xref) - k = util.wavenumber(omega, c) + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + xs = _util.asarray_1d(xs) + xref = _util.asarray_1d(xref) + k = _util.wavenumber(omega, c) ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - d = 1/2 * 1j * k * np.sqrt(xref[1] / (xref[1] - xs[1])) * \ - xs[1] / r * hankel2(1, k * r) - selection = util.source_selection_all(len(x0)) - return d, selection, secondary_source_point(omega, c) + r = _np.linalg.norm(ds, axis=1) + d = 1/2 * 1j * k * _np.sqrt(xref[1] / (xref[1] - xs[1])) * \ + xs[1] / r * _hankel2(1, k * r) + selection = _util.source_selection_all(len(x0)) + return d, selection, _secondary_source_point(omega, c) diff --git a/sfs/fd/source.py b/sfs/fd/source.py index 3a32b86..530ecc4 100644 --- a/sfs/fd/source.py +++ b/sfs/fd/source.py @@ -24,12 +24,13 @@ vgrid = sfs.util.xyz_grid([-2, 3], [-1, 2], 0, spacing=0.1) """ +from itertools import product as _product -import itertools -import numpy as np -from scipy import special -from .. import util -from .. import default +import numpy as _np +from scipy import special as _special + +from .. import default as _default +from .. import util as _util def point(omega, x0, grid, *, c=None): @@ -77,14 +78,14 @@ def point(omega, x0, grid, *, c=None): plt.title("Point Source at {} m (normalized)".format(x0)) """ - k = util.wavenumber(omega, c) - x0 = util.asarray_1d(x0) - grid = util.as_xyz_components(grid) + k = _util.wavenumber(omega, c) + x0 = _util.asarray_1d(x0) + grid = _util.as_xyz_components(grid) - r = np.linalg.norm(grid - x0) + r = _np.linalg.norm(grid - x0) # If r is 0, the sound pressure is complex infinity - numerator = np.exp(-1j * k * r) / (4*np.pi) - with np.errstate(invalid='ignore', divide='ignore'): + numerator = _np.exp(-1j * k * r) / (4 * _np.pi) + with _np.errstate(invalid='ignore', divide='ignore'): return numerator / r @@ -124,17 +125,17 @@ def point_velocity(omega, x0, grid, *, c=None, rho0=None): """ if c is None: - c = default.c + c = _default.c if rho0 is None: - rho0 = default.rho0 - k = util.wavenumber(omega, c) - x0 = util.asarray_1d(x0) - grid = util.as_xyz_components(grid) + rho0 = _default.rho0 + k = _util.wavenumber(omega, c) + x0 = _util.asarray_1d(x0) + grid = _util.as_xyz_components(grid) offset = grid - x0 - r = np.linalg.norm(offset) + r = _np.linalg.norm(offset) v = point(omega, x0, grid, c=c) v *= (1+1j*k*r) / (rho0 * c * 1j*k*r) - return util.XyzComponents([v * o / r for o in offset]) + return _util.XyzComponents([v * o / r for o in offset]) def point_averaged_intensity(omega, x0, grid, *, c=None, rho0=None): @@ -161,15 +162,15 @@ def point_averaged_intensity(omega, x0, grid, *, c=None, rho0=None): """ if c is None: - c = default.c + c = _default.c if rho0 is None: - rho0 = default.rho0 - x0 = util.asarray_1d(x0) - grid = util.as_xyz_components(grid) + rho0 = _default.rho0 + x0 = _util.asarray_1d(x0) + grid = _util.as_xyz_components(grid) offset = grid - x0 - r = np.linalg.norm(offset) + r = _np.linalg.norm(offset) i = 1 / (2 * rho0 * c) - return util.XyzComponents([i * o / r**2 for o in offset]) + return _util.XyzComponents([i * o / r**2 for o in offset]) def point_dipole(omega, x0, n0, grid, *, c=None): @@ -213,15 +214,15 @@ def point_dipole(omega, x0, n0, grid, *, c=None): plt.title("Dipole Point Source at {} m".format(x0)) """ - k = util.wavenumber(omega, c) - x0 = util.asarray_1d(x0) - n0 = util.asarray_1d(n0) - grid = util.as_xyz_components(grid) + k = _util.wavenumber(omega, c) + x0 = _util.asarray_1d(x0) + n0 = _util.asarray_1d(n0) + grid = _util.as_xyz_components(grid) offset = grid - x0 - r = np.linalg.norm(offset) - return 1 / (4*np.pi) * (1j * k + 1 / r) * np.inner(offset, n0) / \ - np.power(r, 2) * np.exp(-1j * k * r) + r = _np.linalg.norm(offset) + return 1 / (4 * _np.pi) * (1j * k + 1 / r) * _np.inner(offset, n0) / \ + _np.power(r, 2) * _np.exp(-1j * k * r) def point_modal(omega, x0, grid, L, *, N=None, deltan=0, c=None): @@ -253,12 +254,12 @@ def point_modal(omega, x0, grid, L, *, N=None, deltan=0, c=None): Sound pressure at positions given by *grid*. """ - k = util.wavenumber(omega, c) - x0 = util.asarray_1d(x0) - x, y, z = util.as_xyz_components(grid) + k = _util.wavenumber(omega, c) + x0 = _util.asarray_1d(x0) + x, y, z = _util.as_xyz_components(grid) - if np.isscalar(N): - N = N * np.ones(3, dtype=int) + if _np.isscalar(N): + N = N * _np.ones(3, dtype=int) if N is None: N = [None, None, None] @@ -267,23 +268,23 @@ def point_modal(omega, x0, grid, L, *, N=None, deltan=0, c=None): for i in range(3): if N[i] is None: # compute max order - orders[i] = range(int(np.ceil(L[i]/np.pi * k) + 1)) - elif np.isscalar(N[i]): + orders[i] = range(int(_np.ceil(L[i] / _np.pi * k) + 1)) + elif _np.isscalar(N[i]): # use given max order orders[i] = range(N[i] + 1) else: # use given orders orders[i] = N[i] - kmp0 = [((kx + 1j * deltan)**2, np.cos(kx * x) * np.cos(kx * x0[0])) - for kx in [m * np.pi / L[0] for m in orders[0]]] - kmp1 = [((ky + 1j * deltan)**2, np.cos(ky * y) * np.cos(ky * x0[1])) - for ky in [n * np.pi / L[1] for n in orders[1]]] - kmp2 = [((kz + 1j * deltan)**2, np.cos(kz * z) * np.cos(kz * x0[2])) - for kz in [l * np.pi / L[2] for l in orders[2]]] + kmp0 = [((kx + 1j * deltan)**2, _np.cos(kx * x) * _np.cos(kx * x0[0])) + for kx in [m * _np.pi / L[0] for m in orders[0]]] + kmp1 = [((ky + 1j * deltan)**2, _np.cos(ky * y) * _np.cos(ky * x0[1])) + for ky in [n * _np.pi / L[1] for n in orders[1]]] + kmp2 = [((kz + 1j * deltan)**2, _np.cos(kz * z) * _np.cos(kz * x0[2])) + for kz in [l * _np.pi / L[2] for l in orders[2]]] ksquared = k**2 p = 0 - for (km0, p0), (km1, p1), (km2, p2) in itertools.product(kmp0, kmp1, kmp2): + for (km0, p0), (km1, p1), (km2, p2) in _product(kmp0, kmp1, kmp2): km = km0 + km1 + km2 p = p + 8 / (ksquared - km) * p0 * p1 * p2 return p @@ -320,19 +321,19 @@ def point_modal_velocity(omega, x0, grid, L, *, N=None, deltan=0, c=None): Particle velocity at positions given by *grid*. """ - k = util.wavenumber(omega, c) - x0 = util.asarray_1d(x0) - x, y, z = util.as_xyz_components(grid) + k = _util.wavenumber(omega, c) + x0 = _util.asarray_1d(x0) + x, y, z = _util.as_xyz_components(grid) if N is None: # determine maximum modal order per dimension - Nx = int(np.ceil(L[0]/np.pi * k)) - Ny = int(np.ceil(L[1]/np.pi * k)) - Nz = int(np.ceil(L[2]/np.pi * k)) + Nx = int(_np.ceil(L[0] / _np.pi * k)) + Ny = int(_np.ceil(L[1] / _np.pi * k)) + Nz = int(_np.ceil(L[2] / _np.pi * k)) mm = range(Nx) nn = range(Ny) ll = range(Nz) - elif np.isscalar(N): + elif _np.isscalar(N): # compute up to a given order mm = range(N) nn = range(N) @@ -343,22 +344,22 @@ def point_modal_velocity(omega, x0, grid, L, *, N=None, deltan=0, c=None): nn = [N[1]] ll = [N[2]] - kmp0 = [((kx + 1j * deltan)**2, np.sin(kx * x) * np.cos(kx * x0[0])) - for kx in [m * np.pi / L[0] for m in mm]] - kmp1 = [((ky + 1j * deltan)**2, np.sin(ky * y) * np.cos(ky * x0[1])) - for ky in [n * np.pi / L[1] for n in nn]] - kmp2 = [((kz + 1j * deltan)**2, np.sin(kz * z) * np.cos(kz * x0[2])) - for kz in [l * np.pi / L[2] for l in ll]] + kmp0 = [((kx + 1j * deltan)**2, _np.sin(kx * x) * _np.cos(kx * x0[0])) + for kx in [m * _np.pi / L[0] for m in mm]] + kmp1 = [((ky + 1j * deltan)**2, _np.sin(ky * y) * _np.cos(ky * x0[1])) + for ky in [n * _np.pi / L[1] for n in nn]] + kmp2 = [((kz + 1j * deltan)**2, _np.sin(kz * z) * _np.cos(kz * x0[2])) + for kz in [l * _np.pi / L[2] for l in ll]] ksquared = k**2 vx = 0+0j vy = 0+0j vz = 0+0j - for (km0, p0), (km1, p1), (km2, p2) in itertools.product(kmp0, kmp1, kmp2): + for (km0, p0), (km1, p1), (km2, p2) in _product(kmp0, kmp1, kmp2): km = km0 + km1 + km2 vx = vx - 8*1j / (ksquared - km) * p0 vy = vy - 8*1j / (ksquared - km) * p1 vz = vz - 8*1j / (ksquared - km) * p2 - return util.XyzComponents([vx, vy, vz]) + return _util.XyzComponents([vx, vy, vz]) def point_image_sources(omega, x0, grid, L, *, max_order, coeffs=None, c=None): @@ -390,16 +391,16 @@ def point_image_sources(omega, x0, grid, L, *, max_order, coeffs=None, c=None): """ if coeffs is None: - coeffs = np.ones(6) + coeffs = _np.ones(6) - xs, order = util.image_sources_for_box(x0, L, max_order) - source_strengths = np.prod(coeffs**order, axis=1) + xs, order = _util.image_sources_for_box(x0, L, max_order) + source_strengths = _np.prod(coeffs**order, axis=1) p = 0 for position, strength in zip(xs, source_strengths): if strength != 0: # point can be complex infinity - with np.errstate(invalid='ignore'): + with _np.errstate(invalid='ignore'): p += strength * point(omega, position, grid, c=c) return p @@ -435,11 +436,11 @@ def line(omega, x0, grid, *, c=None): plt.title("Line Source at {} m (normalized)".format(x0[:2])) """ - k = util.wavenumber(omega, c) - x0 = util.asarray_1d(x0)[:2] # ignore z-component - grid = util.as_xyz_components(grid) + k = _util.wavenumber(omega, c) + x0 = _util.asarray_1d(x0)[:2] # ignore z-component + grid = _util.as_xyz_components(grid) - r = np.linalg.norm(grid[:2] - x0) + r = _np.linalg.norm(grid[:2] - x0) p = -1j/4 * _hankel2_0(k * r) return _duplicate_zdirection(p, grid) @@ -466,24 +467,24 @@ def line_velocity(omega, x0, grid, *, c=None, rho0=None): """ if c is None: - c = default.c + c = _default.c if rho0 is None: - rho0 = default.rho0 - k = util.wavenumber(omega, c) - x0 = util.asarray_1d(x0)[:2] # ignore z-component - grid = util.as_xyz_components(grid) + rho0 = _default.rho0 + k = _util.wavenumber(omega, c) + x0 = _util.asarray_1d(x0)[:2] # ignore z-component + grid = _util.as_xyz_components(grid) offset = grid[:2] - x0 - r = np.linalg.norm(offset) - v = -1/(4 * c * rho0) * special.hankel2(1, k * r) + r = _np.linalg.norm(offset) + v = -1/(4 * c * rho0) * _special.hankel2(1, k * r) v = [v * o / r for o in offset] assert v[0].shape == v[1].shape if len(grid) > 2: - v.append(np.zeros_like(v[0])) + v.append(_np.zeros_like(v[0])) - return util.XyzComponents([_duplicate_zdirection(vi, grid) for vi in v]) + return _util.XyzComponents([_duplicate_zdirection(vi, grid) for vi in v]) def line_dipole(omega, x0, n0, grid, *, c=None): @@ -498,18 +499,18 @@ def line_dipole(omega, x0, n0, grid, *, c=None): G(\x-\x_0,\w) = \frac{\i k}{4} \Hankel{2}{1}{\wc|\x-\x_0|} \cos{\phi} """ - k = util.wavenumber(omega, c) - x0 = util.asarray_1d(x0)[:2] # ignore z-components - n0 = util.asarray_1d(n0)[:2] - grid = util.as_xyz_components(grid) + k = _util.wavenumber(omega, c) + x0 = _util.asarray_1d(x0)[:2] # ignore z-components + n0 = _util.asarray_1d(n0)[:2] + grid = _util.as_xyz_components(grid) dx = grid[:2] - x0 - r = np.linalg.norm(dx) - p = 1j*k/4 * special.hankel2(1, k * r) * np.inner(dx, n0) / r + r = _np.linalg.norm(dx) + p = 1j*k/4 * _special.hankel2(1, k * r) * _np.inner(dx, n0) / r return _duplicate_zdirection(p, grid) -def line_dirichlet_edge(omega, x0, grid, *, alpha=3/2*np.pi, Nc=None, c=None): +def line_dirichlet_edge(omega, x0, grid, *, alpha=_np.pi*3/2, Nc=None, c=None): """Line source scattered at an edge with Dirichlet boundary conditions. :cite:`Moser2012`, eq.(10.18/19) @@ -537,37 +538,37 @@ def line_dirichlet_edge(omega, x0, grid, *, alpha=3/2*np.pi, Nc=None, c=None): Complex pressure at grid positions. """ - k = util.wavenumber(omega, c) - x0 = util.asarray_1d(x0) - phi_s = np.arctan2(x0[1], x0[0]) + k = _util.wavenumber(omega, c) + x0 = _util.asarray_1d(x0) + phi_s = _np.arctan2(x0[1], x0[0]) if phi_s < 0: - phi_s = phi_s + 2*np.pi - r_s = np.linalg.norm(x0) + phi_s = phi_s + 2 * _np.pi + r_s = _np.linalg.norm(x0) - grid = util.XyzComponents(grid) + grid = _util.XyzComponents(grid) - r = np.linalg.norm(grid[:2]) - phi = np.arctan2(grid[1], grid[0]) - phi = np.where(phi < 0, phi+2*np.pi, phi) + r = _np.linalg.norm(grid[:2]) + phi = _np.arctan2(grid[1], grid[0]) + phi = _np.where(phi < 0, phi + 2 * _np.pi, phi) if Nc is None: - Nc = np.ceil(2 * k * np.max(r) * alpha/np.pi) + Nc = _np.ceil(2 * k * _np.max(r) * alpha / _np.pi) - epsilon = np.ones(Nc) # weights for series expansion + epsilon = _np.ones(Nc) # weights for series expansion epsilon[0] = 2 - p = np.zeros((grid[0].shape[1], grid[1].shape[0]), dtype=complex) + p = _np.zeros((grid[0].shape[1], grid[1].shape[0]), dtype=complex) idxr = (r <= r_s) idxa = (phi <= alpha) - for m in np.arange(Nc): - nu = m*np.pi/alpha - f = 1/epsilon[m] * np.sin(nu*phi_s) * np.sin(nu*phi) + for m in _np.arange(Nc): + nu = m * _np.pi / alpha + f = 1/epsilon[m] * _np.sin(nu*phi_s) * _np.sin(nu*phi) p[idxr & idxa] = p[idxr & idxa] + f[idxr & idxa] * \ - special.jn(nu, k*r[idxr & idxa]) * special.hankel2(nu, k*r_s) + _special.jn(nu, k*r[idxr & idxa]) * _special.hankel2(nu, k*r_s) p[~idxr & idxa] = p[~idxr & idxa] + f[~idxr & idxa] * \ - special.jn(nu, k*r_s) * special.hankel2(nu, k*r[~idxr & idxa]) + _special.jn(nu, k*r_s) * _special.hankel2(nu, k*r[~idxr & idxa]) - p = p * -1j*np.pi/alpha + p = p * -1j * _np.pi / alpha pl = line(omega, x0, None, grid, c=c) p[~idxa] = pl[~idxa] @@ -615,11 +616,11 @@ def plane(omega, x0, n0, grid, *, c=None): plt.title("Plane wave with direction {} degree".format(direction)) """ - k = util.wavenumber(omega, c) - x0 = util.asarray_1d(x0) - n0 = util.normalize_vector(n0) - grid = util.as_xyz_components(grid) - return np.exp(-1j * k * np.inner(grid - x0, n0)) + k = _util.wavenumber(omega, c) + x0 = _util.asarray_1d(x0) + n0 = _util.normalize_vector(n0) + grid = _util.as_xyz_components(grid) + return _np.exp(-1j * k * _np.inner(grid - x0, n0)) def plane_velocity(omega, x0, n0, grid, *, c=None, rho0=None): @@ -666,11 +667,11 @@ def plane_velocity(omega, x0, n0, grid, *, c=None, rho0=None): """ if c is None: - c = default.c + c = _default.c if rho0 is None: - rho0 = default.rho0 + rho0 = _default.rho0 v = plane(omega, x0, n0, grid, c=c) / (rho0 * c) - return util.XyzComponents([v * n for n in n0]) + return _util.XyzComponents([v * n for n in n0]) def plane_averaged_intensity(omega, x0, n0, grid, *, c=None, rho0=None): @@ -705,11 +706,11 @@ def plane_averaged_intensity(omega, x0, n0, grid, *, c=None, rho0=None): """ if c is None: - c = default.c + c = _default.c if rho0 is None: - rho0 = default.rho0 + rho0 = _default.rho0 i = 1 / (2 * rho0 * c) - return util.XyzComponents([i * n for n in n0]) + return _util.XyzComponents([i * n for n in n0]) def pulsating_sphere(omega, center, radius, amplitude, grid, *, inside=False, @@ -755,18 +756,18 @@ def pulsating_sphere(omega, center, radius, amplitude, grid, *, inside=False, """ if c is None: - c = default.c - k = util.wavenumber(omega, c) - center = util.asarray_1d(center) - grid = util.as_xyz_components(grid) - - distance = np.linalg.norm(grid - center) - theta = np.arctan(1, k * distance) - impedance = default.rho0 * c * np.cos(theta) * np.exp(1j * theta) + c = _default.c + k = _util.wavenumber(omega, c) + center = _util.asarray_1d(center) + grid = _util.as_xyz_components(grid) + + distance = _np.linalg.norm(grid - center) + theta = _np.arctan(1, k * distance) + impedance = _default.rho0 * c * _np.cos(theta) * _np.exp(1j * theta) radial_velocity = 1j * omega * amplitude * radius / distance \ - * np.exp(-1j * k * (distance - radius)) + * _np.exp(-1j * k * (distance - radius)) if not inside: - radial_velocity[distance <= radius] = np.nan + radial_velocity[distance <= radius] = _np.nan return impedance * radial_velocity @@ -809,24 +810,25 @@ def pulsating_sphere_velocity(omega, center, radius, amplitude, grid, *, """ if c is None: - c = default.c - k = util.wavenumber(omega, c) - grid = util.as_xyz_components(grid) + c = _default.c + k = _util.wavenumber(omega, c) + grid = _util.as_xyz_components(grid) - center = util.asarray_1d(center) + center = _util.asarray_1d(center) offset = grid - center - distance = np.linalg.norm(offset) + distance = _np.linalg.norm(offset) radial_velocity = 1j * omega * amplitude * radius / distance \ - * np.exp(-1j * k * (distance - radius)) - radial_velocity[distance <= radius] = np.nan - return util.XyzComponents([radial_velocity * o / distance for o in offset]) + * _np.exp(-1j * k * (distance - radius)) + radial_velocity[distance <= radius] = _np.nan + return _util.XyzComponents( + [radial_velocity * o / distance for o in offset]) def _duplicate_zdirection(p, grid): """If necessary, duplicate field in z-direction.""" - gridshape = np.broadcast(*grid).shape + gridshape = _np.broadcast(*grid).shape if len(gridshape) > 2: - return np.tile(p, [1, 1, gridshape[2]]) + return _np.tile(p, [1, 1, gridshape[2]]) else: return p @@ -834,4 +836,4 @@ def _duplicate_zdirection(p, grid): def _hankel2_0(x): """Wrapper for Hankel function of the second type using fast versions of the Bessel functions of first/second kind in scipy""" - return special.j0(x)-1j*special.y0(x) + return _special.j0(x) - 1j * _special.y0(x) diff --git a/sfs/fd/wfs.py b/sfs/fd/wfs.py index deaf7e9..817b4eb 100644 --- a/sfs/fd/wfs.py +++ b/sfs/fd/wfs.py @@ -31,12 +31,13 @@ def plot(d, selection, secondary_source): sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15) """ +import numpy as _np +from numpy.core.umath_tests import inner1d as _inner1d +from scipy.special import hankel2 as _hankel2 -import numpy as np -from numpy.core.umath_tests import inner1d # element-wise inner product -from scipy.special import hankel2 -from .. import util -from . import secondary_source_line, secondary_source_point +from . import secondary_source_line as _secondary_source_line +from . import secondary_source_point as _secondary_source_point +from .. import util as _util def line_2d(omega, x0, n0, xs, *, c=None): @@ -84,15 +85,15 @@ def line_2d(omega, x0, n0, xs, *, c=None): plot(d, selection, secondary_source) """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - k = util.wavenumber(omega, c) + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + xs = _util.asarray_1d(xs) + k = _util.wavenumber(omega, c) ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - d = -1j/2 * k * inner1d(ds, n0) / r * hankel2(1, k * r) - selection = util.source_selection_line(n0, x0, xs) - return d, selection, secondary_source_line(omega, c) + r = _np.linalg.norm(ds, axis=1) + d = -1j/2 * k * _inner1d(ds, n0) / r * _hankel2(1, k * r) + selection = _util.source_selection_line(n0, x0, xs) + return d, selection, _secondary_source_line(omega, c) def _point(omega, x0, n0, xs, *, c=None): @@ -140,15 +141,15 @@ def _point(omega, x0, n0, xs, *, c=None): plot(d, selection, secondary_source) """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - k = util.wavenumber(omega, c) + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + xs = _util.asarray_1d(xs) + k = _util.wavenumber(omega, c) ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - d = 1j * k * inner1d(ds, n0) / r ** (3 / 2) * np.exp(-1j * k * r) - selection = util.source_selection_point(n0, x0, xs) - return d, selection, secondary_source_point(omega, c) + r = _np.linalg.norm(ds, axis=1) + d = 1j * k * _inner1d(ds, n0) / r ** (3 / 2) * _np.exp(-1j * k * r) + selection = _util.source_selection_point(n0, x0, xs) + return d, selection, _secondary_source_point(omega, c) point_2d = _point @@ -219,25 +220,25 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): plot(normalize_gain * d, selection, secondary_source) """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - xref = util.asarray_1d(xref) - k = util.wavenumber(omega, c) + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + xs = _util.asarray_1d(xs) + xref = _util.asarray_1d(xref) + k = _util.wavenumber(omega, c) ds = x0 - xs dr = xref - x0 - s = np.linalg.norm(ds, axis=1) - r = np.linalg.norm(dr, axis=1) + s = _np.linalg.norm(ds, axis=1) + r = _np.linalg.norm(dr, axis=1) d = ( preeq_25d(omega, omalias, c) * - np.sqrt(8 * np.pi) * - np.sqrt((r * s) / (r + s)) * - inner1d(n0, ds) / s * - np.exp(-1j * k * s) / (4 * np.pi * s)) - selection = util.source_selection_point(n0, x0, xs) - return d, selection, secondary_source_point(omega, c) + _np.sqrt(8 * _np.pi) * + _np.sqrt((r * s) / (r + s)) * + _inner1d(n0, ds) / s * + _np.exp(-1j * k * s) / (4 * _np.pi * s)) + selection = _util.source_selection_point(n0, x0, xs) + return d, selection, _secondary_source_point(omega, c) point_3d = _point @@ -307,19 +308,19 @@ def point_25d_legacy(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): plot(normalize_gain * d, selection, secondary_source) """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - xref = util.asarray_1d(xref) - k = util.wavenumber(omega, c) + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + xs = _util.asarray_1d(xs) + xref = _util.asarray_1d(xref) + k = _util.wavenumber(omega, c) ds = x0 - xs - r = np.linalg.norm(ds, axis=1) + r = _np.linalg.norm(ds, axis=1) d = ( preeq_25d(omega, omalias, c) * - np.sqrt(np.linalg.norm(xref - x0)) * inner1d(ds, n0) / - r ** (3 / 2) * np.exp(-1j * k * r)) - selection = util.source_selection_point(n0, x0, xs) - return d, selection, secondary_source_point(omega, c) + _np.sqrt(_np.linalg.norm(xref - x0)) * _inner1d(ds, n0) / + r ** (3 / 2) * _np.exp(-1j * k * r)) + selection = _util.source_selection_point(n0, x0, xs) + return d, selection, _secondary_source_point(omega, c) def _plane(omega, x0, n0, n=[0, 1, 0], *, c=None): @@ -368,13 +369,13 @@ def _plane(omega, x0, n0, n=[0, 1, 0], *, c=None): plot(d, selection, secondary_source) """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - n = util.normalize_vector(n) - k = util.wavenumber(omega, c) - d = 2j * k * np.inner(n, n0) * np.exp(-1j * k * np.inner(n, x0)) - selection = util.source_selection_plane(n0, n) - return d, selection, secondary_source_point(omega, c) + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + n = _util.normalize_vector(n) + k = _util.wavenumber(omega, c) + d = 2j * k * _np.inner(n, n0) * _np.exp(-1j * k * _np.inner(n, x0)) + selection = _util.source_selection_plane(n0, n) + return d, selection, _secondary_source_point(omega, c) plane_2d = _plane @@ -430,17 +431,17 @@ def plane_25d(omega, x0, n0, n=[0, 1, 0], *, xref=[0, 0, 0], c=None, plot(d, selection, secondary_source) """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - n = util.normalize_vector(n) - xref = util.asarray_1d(xref) - k = util.wavenumber(omega, c) + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + n = _util.normalize_vector(n) + xref = _util.asarray_1d(xref) + k = _util.wavenumber(omega, c) d = ( preeq_25d(omega, omalias, c) * - np.sqrt(8*np.pi * np.linalg.norm(xref - x0, axis=-1)) * - np.inner(n, n0) * np.exp(-1j * k * np.inner(n, x0))) - selection = util.source_selection_plane(n0, n) - return d, selection, secondary_source_point(omega, c) + _np.sqrt(8 * _np.pi * _np.linalg.norm(xref - x0, axis=-1)) * + _np.inner(n, n0) * _np.exp(-1j * k * _np.inner(n, x0))) + selection = _util.source_selection_plane(n0, n) + return d, selection, _secondary_source_point(omega, c) plane_3d = _plane @@ -493,15 +494,15 @@ def _focused(omega, x0, n0, xs, ns, *, c=None): plot(d, selection, secondary_source) """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - k = util.wavenumber(omega, c) + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + xs = _util.asarray_1d(xs) + k = _util.wavenumber(omega, c) ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - d = 1j * k * inner1d(ds, n0) / r ** (3 / 2) * np.exp(1j * k * r) - selection = util.source_selection_focused(ns, x0, xs) - return d, selection, secondary_source_point(omega, c) + r = _np.linalg.norm(ds, axis=1) + d = 1j * k * _inner1d(ds, n0) / r ** (3 / 2) * _np.exp(1j * k * r) + selection = _util.source_selection_focused(ns, x0, xs) + return d, selection, _secondary_source_point(omega, c) focused_2d = _focused @@ -560,19 +561,19 @@ def focused_25d(omega, x0, n0, xs, ns, *, xref=[0, 0, 0], c=None, plot(d, selection, secondary_source) """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - xref = util.asarray_1d(xref) - k = util.wavenumber(omega, c) + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + xs = _util.asarray_1d(xs) + xref = _util.asarray_1d(xref) + k = _util.wavenumber(omega, c) ds = x0 - xs - r = np.linalg.norm(ds, axis=1) + r = _np.linalg.norm(ds, axis=1) d = ( preeq_25d(omega, omalias, c) * - np.sqrt(np.linalg.norm(xref - x0)) * inner1d(ds, n0) / - r ** (3 / 2) * np.exp(1j * k * r)) - selection = util.source_selection_focused(ns, x0, xs) - return d, selection, secondary_source_point(omega, c) + _np.sqrt(_np.linalg.norm(xref - x0)) * _inner1d(ds, n0) / + r ** (3 / 2) * _np.exp(1j * k * r)) + selection = _util.source_selection_focused(ns, x0, xs) + return d, selection, _secondary_source_point(omega, c) focused_3d = _focused @@ -607,12 +608,12 @@ def preeq_25d(omega, omalias, c): """ if omalias is None: - return np.sqrt(1j * util.wavenumber(omega, c)) + return _np.sqrt(1j * _util.wavenumber(omega, c)) else: if omega <= omalias: - return np.sqrt(1j * util.wavenumber(omega, c)) + return _np.sqrt(1j * _util.wavenumber(omega, c)) else: - return np.sqrt(1j * util.wavenumber(omalias, c)) + return _np.sqrt(1j * _util.wavenumber(omalias, c)) def plane_3d_delay(omega, x0, n0, n=[0, 1, 0], *, c=None): @@ -658,12 +659,12 @@ def plane_3d_delay(omega, x0, n0, n=[0, 1, 0], *, c=None): plot(d, selection, secondary_source) """ - x0 = util.asarray_of_rows(x0) - n = util.normalize_vector(n) - k = util.wavenumber(omega, c) - d = np.exp(-1j * k * np.inner(n, x0)) - selection = util.source_selection_plane(n0, n) - return d, selection, secondary_source_point(omega, c) + x0 = _util.asarray_of_rows(x0) + n = _util.normalize_vector(n) + k = _util.wavenumber(omega, c) + d = _np.exp(-1j * k * _np.inner(n, x0)) + selection = _util.source_selection_plane(n0, n) + return d, selection, _secondary_source_point(omega, c) def soundfigure_3d(omega, x0, n0, figure, npw=[0, 0, 1], *, c=None): @@ -673,35 +674,35 @@ def soundfigure_3d(omega, x0, n0, figure, npw=[0, 0, 1], *, c=None): [Helwani et al., The Synthesis of Sound Figures, MSSP, 2013] """ - x0 = np.asarray(x0) - n0 = np.asarray(n0) - k = util.wavenumber(omega, c) + x0 = _np.asarray(x0) + n0 = _np.asarray(n0) + k = _util.wavenumber(omega, c) nx, ny = figure.shape # 2D spatial DFT of image - figure = np.fft.fftshift(figure, axes=(0, 1)) # sign of spatial DFT - figure = np.fft.fft2(figure) + figure = _np.fft.fftshift(figure, axes=(0, 1)) # sign of spatial DFT + figure = _np.fft.fft2(figure) # wavenumbers - kx = np.fft.fftfreq(nx, 1./nx) - ky = np.fft.fftfreq(ny, 1./ny) + kx = _np.fft.fftfreq(nx, 1./nx) + ky = _np.fft.fftfreq(ny, 1./ny) # shift spectrum due to desired plane wave - figure = np.roll(figure, int(k*npw[0]), axis=0) - figure = np.roll(figure, int(k*npw[1]), axis=1) + figure = _np.roll(figure, int(k*npw[0]), axis=0) + figure = _np.roll(figure, int(k*npw[1]), axis=1) # search and iterate over propagating plane wave components - kxx, kyy = np.meshgrid(kx, ky, sparse=True) - rho = np.sqrt((kxx) ** 2 + (kyy) ** 2) + kxx, kyy = _np.meshgrid(kx, ky, sparse=True) + rho = _np.sqrt((kxx) ** 2 + (kyy) ** 2) d = 0 for n in range(nx): for m in range(ny): if(rho[n, m] < k): # dispertion relation - kz = np.sqrt(k**2 - rho[n, m]**2) + kz = _np.sqrt(k**2 - rho[n, m]**2) # normal vector of plane wave - npw = 1/k * np.asarray([kx[n], ky[m], kz]) - npw = npw / np.linalg.norm(npw) + npw = 1/k * _np.asarray([kx[n], ky[m], kz]) + npw = npw / _np.linalg.norm(npw) # driving function of plane wave with positive kz d_component, selection, secondary_source = plane_3d( omega, x0, n0, npw, c=c) d += selection * figure[n, m] * d_component - return d, util.source_selection_all(len(d)), secondary_source + return d, _util.source_selection_all(len(d)), secondary_source diff --git a/sfs/plot2d.py b/sfs/plot2d.py index 0bd86c7..f05936e 100644 --- a/sfs/plot2d.py +++ b/sfs/plot2d.py @@ -1,26 +1,24 @@ """2D plots of sound fields etc.""" -import matplotlib.pyplot as plt -from matplotlib import __version__ as matplotlib_version -from matplotlib.patches import PathPatch -from matplotlib.path import Path -from matplotlib.collections import PatchCollection -from mpl_toolkits import axes_grid1 -import numpy as np -from . import util -from . import default +import matplotlib as _mpl +import matplotlib.pyplot as _plt +from mpl_toolkits import axes_grid1 as _axes_grid1 +import numpy as _np + +from . import default as _default +from . import util as _util def _register_cmap_clip(name, original_cmap, alpha): """Create a color map with "over" and "under" values.""" from matplotlib.colors import LinearSegmentedColormap - cdata = plt.cm.datad[original_cmap] + cdata = _plt.cm.datad[original_cmap] if isinstance(cdata, dict): cmap = LinearSegmentedColormap(name, cdata) else: cmap = LinearSegmentedColormap.from_list(name, cdata) cmap.set_over([alpha * c + 1 - alpha for c in cmap(1.0)[:3]]) cmap.set_under([alpha * c + 1 - alpha for c in cmap(0.0)[:3]]) - plt.cm.register_cmap(cmap=cmap) + _plt.cm.register_cmap(cmap=cmap) # The 'coolwarm' colormap is based on the paper @@ -38,7 +36,7 @@ def _register_cmap_transparent(name, color): 'blue': ((0, blue, blue), (1, blue, blue)), 'alpha': ((0, 0, 0), (1, 1, 1))} cmap = LinearSegmentedColormap(name, cdict) - plt.cm.register_cmap(cmap=cmap) + _plt.cm.register_cmap(cmap=cmap) _register_cmap_transparent('blacktransparent', 'black') @@ -46,16 +44,16 @@ def _register_cmap_transparent(name, color): def virtualsource(xs, ns=None, type='point', *, ax=None): """Draw position/orientation of virtual source.""" - xs = np.asarray(xs) - ns = np.asarray(ns) + xs = _np.asarray(xs) + ns = _np.asarray(ns) if ax is None: - ax = plt.gca() + ax = _plt.gca() if type == 'point': - vps = plt.Circle(xs, .05, edgecolor='k', facecolor='k') + vps = _plt.Circle(xs, .05, edgecolor='k', facecolor='k') ax.add_artist(vps) for n in range(1, 3): - vps = plt.Circle(xs, .05+n*0.05, edgecolor='k', fill=False) + vps = _plt.Circle(xs, .05+n*0.05, edgecolor='k', fill=False) ax.add_artist(vps) elif type == 'plane': ns = 0.2 * ns @@ -66,9 +64,9 @@ def virtualsource(xs, ns=None, type='point', *, ax=None): def reference(xref, *, size=0.1, ax=None): """Draw reference/normalization point.""" - xref = np.asarray(xref) + xref = _np.asarray(xref) if ax is None: - ax = plt.gca() + ax = _plt.gca() ax.plot((xref[0]-size, xref[0]+size), (xref[1]-size, xref[1]+size), 'k-') ax.plot((xref[0]-size, xref[0]+size), (xref[1]+size, xref[1]-size), 'k-') @@ -76,9 +74,9 @@ def reference(xref, *, size=0.1, ax=None): def secondary_sources(x0, n0, *, grid=None): """Simple plot of secondary source locations.""" - x0 = np.asarray(x0) - n0 = np.asarray(n0) - ax = plt.gca() + x0 = _np.asarray(x0) + n0 = _np.asarray(n0) + ax = _plt.gca() # plot only secondary sources inside simulated area if grid is not None: @@ -86,7 +84,7 @@ def secondary_sources(x0, n0, *, grid=None): # plot symbols for x00 in x0: - ss = plt.Circle(x00[0:2], .05, edgecolor='k', facecolor='k') + ss = _plt.Circle(x00[0:2], .05, edgecolor='k', facecolor='k') ax.add_artist(ss) @@ -113,9 +111,9 @@ def loudspeakers(x0, n0, a0=0.5, *, size=0.08, show_numbers=False, grid=None, object or -- if not specified -- into the current axes. """ - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - a0 = util.asarray_1d(a0).reshape(-1, 1) + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + a0 = _util.asarray_1d(a0).reshape(-1, 1) # plot only secondary sources inside simulated area if grid is not None: @@ -123,35 +121,37 @@ def loudspeakers(x0, n0, a0=0.5, *, size=0.08, show_numbers=False, grid=None, # normalized coordinates of loudspeaker symbol (see IEC 60617-9) codes, coordinates = zip(*( - (Path.MOVETO, [-0.62, 0.21]), - (Path.LINETO, [-0.31, 0.21]), - (Path.LINETO, [0, 0.5]), - (Path.LINETO, [0, -0.5]), - (Path.LINETO, [-0.31, -0.21]), - (Path.LINETO, [-0.62, -0.21]), - (Path.CLOSEPOLY, [0, 0]), - (Path.MOVETO, [-0.31, 0.21]), - (Path.LINETO, [-0.31, -0.21]), + (_mpl.path.Path.MOVETO, [-0.62, 0.21]), + (_mpl.path.Path.LINETO, [-0.31, 0.21]), + (_mpl.path.Path.LINETO, [0, 0.5]), + (_mpl.path.Path.LINETO, [0, -0.5]), + (_mpl.path.Path.LINETO, [-0.31, -0.21]), + (_mpl.path.Path.LINETO, [-0.62, -0.21]), + (_mpl.path.Path.CLOSEPOLY, [0, 0]), + (_mpl.path.Path.MOVETO, [-0.31, 0.21]), + (_mpl.path.Path.LINETO, [-0.31, -0.21]), )) - coordinates = np.column_stack([coordinates, np.zeros(len(coordinates))]) + coordinates = _np.column_stack([coordinates, _np.zeros(len(coordinates))]) coordinates *= size patches = [] - for x00, n00 in util.broadcast_zip(x0, n0): + for x00, n00 in _util.broadcast_zip(x0, n0): # rotate and translate coordinates - R = util.rotation_matrix([1, 0, 0], n00) - transformed_coordinates = np.inner(coordinates, R) + x00 + R = _util.rotation_matrix([1, 0, 0], n00) + transformed_coordinates = _np.inner(coordinates, R) + x00 - patches.append(PathPatch(Path(transformed_coordinates[:, :2], codes))) + patches.append(_mpl.patches.PathPatch(_mpl.path.Path( + transformed_coordinates[:, :2], codes))) # add collection of patches to current axis - p = PatchCollection(patches, edgecolor='0', facecolor=np.tile(1 - a0, 3)) + p = _mpl.collections.PatchCollection( + patches, edgecolor='0', facecolor=_np.tile(1 - a0, 3)) if ax is None: - ax = plt.gca() + ax = _plt.gca() ax.add_collection(p) if show_numbers: - for idx, (x00, n00) in enumerate(util.broadcast_zip(x0, n0)): + for idx, (x00, n00) in enumerate(_util.broadcast_zip(x0, n0)): x, y = x00[:2] - 1.2 * size * n00[:2] ax.text(x, y, idx + 1, horizontalalignment='center', verticalalignment='center', clip_on=True) @@ -159,10 +159,10 @@ def loudspeakers(x0, n0, a0=0.5, *, size=0.08, show_numbers=False, grid=None, def _visible_secondarysources(x0, n0, grid): """Determine secondary sources which lie within *grid*.""" - x, y = util.as_xyz_components(grid[:2]) - idx = np.where((x0[:, 0] > x.min()) & (x0[:, 0] < x.max()) & + x, y = _util.as_xyz_components(grid[:2]) + idx = _np.where((x0[:, 0] > x.min()) & (x0[:, 0] < x.max()) & (x0[:, 1] > y.min()) & (x0[:, 1] < x.max())) - idx = np.squeeze(idx) + idx = _np.squeeze(idx) return x0[idx, :], n0[idx, :] @@ -233,12 +233,12 @@ def amplitude(p, grid, *, xnorm=None, cmap='coolwarm_clip', sfs.plot2d.level """ - p = np.asarray(p) - grid = util.as_xyz_components(grid) + p = _np.asarray(p) + grid = _util.as_xyz_components(grid) # normalize sound field wrt xnorm if xnorm is not None: - p = util.normalize(p, grid, xnorm) + p = _util.normalize(p, grid, xnorm) if p.ndim == 3: if p.shape[2] == 1: @@ -277,13 +277,13 @@ def amplitude(p, grid, *, xnorm=None, cmap='coolwarm_clip', dy = 0.5 * y.ptp() / p.shape[1] if ax is None: - ax = plt.gca() + ax = _plt.gca() # see https://github.com/matplotlib/matplotlib/issues/10567 - if matplotlib_version.startswith('2.1.'): - p = np.clip(p, -1e15, 1e15) # clip to float64 range + if _mpl.__version__.startswith('2.1.'): + p = _np.clip(p, -1e15, 1e15) # clip to float64 range - im = ax.imshow(np.real(p), cmap=cmap, origin='lower', + im = ax.imshow(_np.real(p), cmap=cmap, origin='lower', extent=[x.min()-dx, x.max()+dx, y.min()-dy, y.max()+dy], vmax=vmax, vmin=vmin, **kwargs) if xlabel is None: @@ -311,8 +311,8 @@ def level(p, grid, *, xnorm=None, power=False, cmap=None, vmax=3, vmin=-50, """ # normalize before converting to dB! if xnorm is not None: - p = util.normalize(p, grid, xnorm) - L = util.db(p, power=power) + p = _util.normalize(p, grid, xnorm) + L = _util.db(p, power=power) return amplitude(L, grid=grid, xnorm=None, cmap=cmap, vmax=vmax, vmin=vmin, **kwargs) @@ -320,17 +320,17 @@ def level(p, grid, *, xnorm=None, power=False, cmap=None, vmax=3, vmin=-50, def particles(x, *, trim=None, ax=None, xlabel='x (m)', ylabel='y (m)', edgecolor='', marker='.', s=15, **kwargs): """Plot particle positions as scatter plot""" - XX, YY = [np.real(c) for c in x[:2]] + XX, YY = [_np.real(c) for c in x[:2]] if trim is not None: xmin, xmax, ymin, ymax = trim - idx = np.where((XX > xmin) & (XX < xmax) & (YY > ymin) & (YY < ymax)) + idx = _np.where((XX > xmin) & (XX < xmax) & (YY > ymin) & (YY < ymax)) XX = XX[idx] YY = YY[idx] if ax is None: - ax = plt.gca() + ax = _plt.gca() if xlabel: ax.set_xlabel(xlabel) @@ -372,15 +372,15 @@ def vectors(v, grid, *, cmap='blacktransparent', headlength=3, :func:`matplotlib.pyplot.quiver`. """ - v = util.as_xyz_components(v[:2]).apply(np.real) - X, Y = util.as_xyz_components(grid[:2]) - speed = np.linalg.norm(v) - with np.errstate(invalid='ignore'): - U, V = v.apply(np.true_divide, speed) + v = _util.as_xyz_components(v[:2]).apply(_np.real) + X, Y = _util.as_xyz_components(grid[:2]) + speed = _np.linalg.norm(v) + with _np.errstate(invalid='ignore'): + U, V = v.apply(_np.true_divide, speed) if ax is None: - ax = plt.gca() + ax = _plt.gca() if clim is None: - v_ref = 1 / (default.rho0 * default.c) # reference particle velocity + v_ref = 1 / (_default.rho0 * _default.c) # reference particle velocity clim = 0, 2 * v_ref return ax.quiver(X, Y, U, V, speed, cmap=cmap, pivot='mid', units='xy', angles='xy', headlength=headlength, @@ -417,10 +417,10 @@ def add_colorbar(im, *, aspect=20, pad=0.5, **kwargs): """ ax = im.axes - divider = axes_grid1.make_axes_locatable(ax) - width = axes_grid1.axes_size.AxesY(ax, aspect=1/aspect) - pad = axes_grid1.axes_size.Fraction(pad, width) - current_ax = plt.gca() + divider = _axes_grid1.make_axes_locatable(ax) + width = _axes_grid1.axes_size.AxesY(ax, aspect=1/aspect) + pad = _axes_grid1.axes_size.Fraction(pad, width) + current_ax = _plt.gca() cax = divider.append_axes("right", size=width, pad=pad) - plt.sca(current_ax) + _plt.sca(current_ax) return ax.figure.colorbar(im, cax=cax, orientation='vertical', **kwargs) diff --git a/sfs/plot3d.py b/sfs/plot3d.py index 617da51..063a1d1 100644 --- a/sfs/plot3d.py +++ b/sfs/plot3d.py @@ -1,15 +1,14 @@ """3D plots of sound fields etc.""" -import matplotlib.pyplot as plt -from mpl_toolkits.mplot3d import Axes3D +import matplotlib.pyplot as _plt def secondary_sources(x0, n0, a0=None, *, w=0.08, h=0.08): """Plot positions and normals of a 3D secondary source distribution.""" - fig = plt.figure(figsize=(15, 15)) + fig = _plt.figure(figsize=(15, 15)) ax = fig.add_subplot(111, projection='3d') q = ax.quiver(x0[:, 0], x0[:, 1], x0[:, 2], n0[:, 0], n0[:, 1], n0[:, 2], length=0.1) - plt.xlabel('x (m)') - plt.ylabel('y (m)') - plt.title('Secondary Sources') + _plt.xlabel('x (m)') + _plt.ylabel('y (m)') + _plt.title('Secondary Sources') return q diff --git a/sfs/tapering.py b/sfs/tapering.py index 9b34c8f..20f3226 100644 --- a/sfs/tapering.py +++ b/sfs/tapering.py @@ -17,7 +17,7 @@ active2[30:-10] = False """ -import numpy as np +import numpy as _np def none(active): @@ -91,20 +91,20 @@ def tukey(active, *, alpha): """ idx = _windowidx(active) - alpha = np.clip(alpha, 0, 1) + alpha = _np.clip(alpha, 0, 1) if alpha == 0: return none(active) # design Tukey window - x = np.linspace(0, 1, len(idx) + 2) - tukey = np.ones_like(x) + x = _np.linspace(0, 1, len(idx) + 2) + tukey = _np.ones_like(x) first_part = x < alpha / 2 tukey[first_part] = 0.5 * ( - 1 + np.cos(2 * np.pi / alpha * (x[first_part] - alpha / 2))) + 1 + _np.cos(2 * _np.pi / alpha * (x[first_part] - alpha / 2))) third_part = x >= (1 - alpha / 2) tukey[third_part] = 0.5 * ( - 1 + np.cos(2 * np.pi / alpha * (x[third_part] - 1 + alpha / 2))) + 1 + _np.cos(2 * _np.pi / alpha * (x[third_part] - 1 + alpha / 2))) # fit window into tapering function - result = np.zeros(len(active)) + result = _np.zeros(len(active)) result[idx] = tukey[1:-1] return result @@ -147,8 +147,8 @@ def kaiser(active, *, beta): """ idx = _windowidx(active) - window = np.zeros(len(active)) - window[idx] = np.kaiser(len(idx), beta) + window = _np.zeros(len(active)) + window[idx] = _np.kaiser(len(idx), beta) return window @@ -159,11 +159,11 @@ def _windowidx(active): """ # find index where active loudspeakers begin (works for connected contours) - if (active[0] and not active[-1]) or np.all(active): + if (active[0] and not active[-1]) or _np.all(active): first_idx = 0 else: - first_idx = np.argmax(np.diff(active.astype(int))) + 1 + first_idx = _np.argmax(_np.diff(active.astype(int))) + 1 # shift generic index vector to get a connected list of indices - idx = np.roll(np.arange(len(active)), -first_idx) + idx = _np.roll(_np.arange(len(active)), -first_idx) # remove indices of inactive secondary sources - return idx[:np.count_nonzero(active)] + return idx[:_np.count_nonzero(active)] diff --git a/sfs/td/__init__.py b/sfs/td/__init__.py index 6770b5c..5efb20a 100644 --- a/sfs/td/__init__.py +++ b/sfs/td/__init__.py @@ -9,9 +9,10 @@ nfchoa """ -from .. import array as _array -import numpy as np +import numpy as _np + from . import source +from .. import array as _array from .. import util as _util @@ -90,10 +91,10 @@ def apply_delays(signal, delays): delays = _util.asarray_1d(delays) delays += initial_offset - delays_samples = np.rint(samplerate * delays).astype(int) + delays_samples = _np.rint(samplerate * delays).astype(int) offset_samples = delays_samples.min() delays_samples -= offset_samples - out = np.zeros((delays_samples.max() + len(data), len(delays_samples))) + out = _np.zeros((delays_samples.max() + len(data), len(delays_samples))) for column, row in enumerate(delays_samples): out[row:row + len(data), column] = data return _util.DelayedSignal(out, samplerate, offset_samples / samplerate) diff --git a/sfs/td/nfchoa.py b/sfs/td/nfchoa.py index 2e65450..87a2e5f 100644 --- a/sfs/td/nfchoa.py +++ b/sfs/td/nfchoa.py @@ -36,12 +36,13 @@ def plot(d, selection, secondary_source, t=0): sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15) """ -import numpy as np -from scipy.signal import besselap, sosfilt, zpk2sos -from scipy.special import eval_legendre as legendre -from .. import default -from .. import util -from . import secondary_source_point +import numpy as _np +import scipy.signal as _sig +from scipy.special import eval_legendre as _legendre + +from . import secondary_source_point as _secondary_source_point +from .. import default as _default +from .. import util as _util def matchedz_zpk(s_zeros, s_poles, s_gain, fs): @@ -72,12 +73,12 @@ def matchedz_zpk(s_zeros, s_poles, s_gain, fs): :func:`scipy.signal.bilinear_zpk` """ - z_zeros = np.exp(s_zeros / fs) - z_poles = np.exp(s_poles / fs) - omega = 1j * np.pi * fs - s_gain *= np.prod((omega - s_zeros) / (omega - s_poles) - * (-1 - z_poles) / (-1 - z_zeros)) - return z_zeros, z_poles, np.real(s_gain) + z_zeros = _np.exp(s_zeros / fs) + z_poles = _np.exp(s_poles / fs) + omega = 1j * _np.pi * fs + s_gain *= _np.prod((omega - s_zeros) / (omega - s_poles) + * (-1 - z_poles) / (-1 - z_zeros)) + return z_zeros, z_poles, _np.real(s_gain) def plane_25d(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk): @@ -151,28 +152,29 @@ def plane_25d(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk): """ if max_order is None: - max_order = util.max_order_circular_harmonics(len(x0)) + max_order = _util.max_order_circular_harmonics(len(x0)) if c is None: - c = default.c + c = _default.c - x0 = util.asarray_of_rows(x0) - npw = util.asarray_1d(npw) - phi0, _, _ = util.cart2sph(*x0.T) - phipw, _, _ = util.cart2sph(*npw) - phaseshift = phi0 - phipw + np.pi + x0 = _util.asarray_of_rows(x0) + npw = _util.asarray_1d(npw) + phi0, _, _ = _util.cart2sph(*x0.T) + phipw, _, _ = _util.cart2sph(*npw) + phaseshift = phi0 - phipw + _np.pi delay = -r0 / c weight = 2 sos = [] for m in range(max_order + 1): - _, p, _ = besselap(m, norm='delay') - s_zeros = np.zeros(m) + _, p, _ = _sig.besselap(m, norm='delay') + s_zeros = _np.zeros(m) s_poles = c / r0 * p s_gain = 1 z_zeros, z_poles, z_gain = s2z(s_zeros, s_poles, s_gain, fs) - sos.append(zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest')) - selection = util.source_selection_all(len(x0)) - return delay, weight, sos, phaseshift, selection, secondary_source_point(c) + sos.append(_sig.zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest')) + selection = _util.source_selection_all(len(x0)) + return (delay, weight, sos, phaseshift, selection, + _secondary_source_point(c)) def point_25d(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): @@ -247,28 +249,29 @@ def point_25d(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): """ if max_order is None: - max_order = util.max_order_circular_harmonics(len(x0)) + max_order = _util.max_order_circular_harmonics(len(x0)) if c is None: - c = default.c + c = _default.c - x0 = util.asarray_of_rows(x0) - xs = util.asarray_1d(xs) - phi0, _, _ = util.cart2sph(*x0.T) - phis, _, rs = util.cart2sph(*xs) + x0 = _util.asarray_of_rows(x0) + xs = _util.asarray_1d(xs) + phi0, _, _ = _util.cart2sph(*x0.T) + phis, _, rs = _util.cart2sph(*xs) phaseshift = phi0 - phis delay = (rs - r0) / c - weight = 1 / 2 / np.pi / rs + weight = 1 / 2 / _np.pi / rs sos = [] for m in range(max_order + 1): - _, p, _ = besselap(m, norm='delay') + _, p, _ = _sig.besselap(m, norm='delay') s_zeros = c / rs * p s_poles = c / r0 * p s_gain = 1 z_zeros, z_poles, z_gain = s2z(s_zeros, s_poles, s_gain, fs) - sos.append(zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest')) - selection = util.source_selection_all(len(x0)) - return delay, weight, sos, phaseshift, selection, secondary_source_point(c) + sos.append(_sig.zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest')) + selection = _util.source_selection_all(len(x0)) + return (delay, weight, sos, phaseshift, selection, + _secondary_source_point(c)) def plane_3d(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk): @@ -333,28 +336,29 @@ def plane_3d(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk): """ if max_order is None: - max_order = util.max_order_spherical_harmonics(len(x0)) + max_order = _util.max_order_spherical_harmonics(len(x0)) if c is None: - c = default.c + c = _default.c - x0 = util.asarray_of_rows(x0) - npw = util.asarray_1d(npw) - phi0, theta0, _ = util.cart2sph(*x0.T) - phipw, thetapw, _ = util.cart2sph(*npw) - phaseshift = np.arccos(np.dot(x0 / r0, -npw)) + x0 = _util.asarray_of_rows(x0) + npw = _util.asarray_1d(npw) + phi0, theta0, _ = _util.cart2sph(*x0.T) + phipw, thetapw, _ = _util.cart2sph(*npw) + phaseshift = _np.arccos(_np.dot(x0 / r0, -npw)) delay = -r0 / c - weight = 4 * np.pi / r0 + weight = 4 * _np.pi / r0 sos = [] for m in range(max_order + 1): - _, p, _ = besselap(m, norm='delay') - s_zeros = np.zeros(m) + _, p, _ = _sig.besselap(m, norm='delay') + s_zeros = _np.zeros(m) s_poles = c / r0 * p s_gain = 1 z_zeros, z_poles, z_gain = s2z(s_zeros, s_poles, s_gain, fs) - sos.append(zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest')) - selection = util.source_selection_all(len(x0)) - return delay, weight, sos, phaseshift, selection, secondary_source_point(c) + sos.append(_sig.zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest')) + selection = _util.source_selection_all(len(x0)) + return (delay, weight, sos, phaseshift, selection, + _secondary_source_point(c)) def point_3d(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): @@ -420,28 +424,29 @@ def point_3d(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk): """ if max_order is None: - max_order = util.max_order_spherical_harmonics(len(x0)) + max_order = _util.max_order_spherical_harmonics(len(x0)) if c is None: - c = default.c + c = _default.c - x0 = util.asarray_of_rows(x0) - xs = util.asarray_1d(xs) - phi0, theta0, _ = util.cart2sph(*x0.T) - phis, thetas, rs = util.cart2sph(*xs) - phaseshift = np.arccos(np.dot(x0 / r0, xs / rs)) + x0 = _util.asarray_of_rows(x0) + xs = _util.asarray_1d(xs) + phi0, theta0, _ = _util.cart2sph(*x0.T) + phis, thetas, rs = _util.cart2sph(*xs) + phaseshift = _np.arccos(_np.dot(x0 / r0, xs / rs)) delay = (rs - r0) / c weight = 1 / r0 / rs sos = [] for m in range(max_order + 1): - _, p, _ = besselap(m, norm='delay') + _, p, _ = _sig.besselap(m, norm='delay') s_zeros = c / rs * p s_poles = c / r0 * p s_gain = 1 z_zeros, z_poles, z_gain = s2z(s_zeros, s_poles, s_gain, fs) - sos.append(zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest')) - selection = util.source_selection_all(len(x0)) - return delay, weight, sos, phaseshift, selection, secondary_source_point(c) + sos.append(_sig.zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest')) + selection = _util.source_selection_all(len(x0)) + return (delay, weight, sos, phaseshift, selection, + _secondary_source_point(c)) def driving_signals_25d(delay, weight, sos, phaseshift, signal): @@ -469,13 +474,13 @@ def driving_signals_25d(delay, weight, sos, phaseshift, signal): and a (possibly negative) time offset (in seconds). """ - data, fs, t_offset = util.as_delayed_signal(signal) + data, fs, t_offset = _util.as_delayed_signal(signal) N = len(phaseshift) - out = np.tile(np.expand_dims(sosfilt(sos[0], data), 1), (1, N)) + out = _np.tile(_np.expand_dims(_sig.sosfilt(sos[0], data), 1), (1, N)) for m in range(1, len(sos)): - modal_response = sosfilt(sos[m], data)[:, np.newaxis] - out += modal_response * np.cos(m * phaseshift) - return util.DelayedSignal(2 * weight * out, fs, t_offset + delay) + modal_response = _sig.sosfilt(sos[m], data)[:, _np.newaxis] + out += modal_response * _np.cos(m * phaseshift) + return _util.DelayedSignal(2 * weight * out, fs, t_offset + delay) def driving_signals_3d(delay, weight, sos, phaseshift, signal): @@ -503,10 +508,10 @@ def driving_signals_3d(delay, weight, sos, phaseshift, signal): and a (possibly negative) time offset (in seconds). """ - data, fs, t_offset = util.as_delayed_signal(signal) + data, fs, t_offset = _util.as_delayed_signal(signal) N = len(phaseshift) - out = np.tile(np.expand_dims(sosfilt(sos[0], data), 1), (1, N)) + out = _np.tile(_np.expand_dims(_sig.sosfilt(sos[0], data), 1), (1, N)) for m in range(1, len(sos)): - modal_response = sosfilt(sos[m], data)[:, np.newaxis] - out += (2 * m + 1) * modal_response * legendre(m, np.cos(phaseshift)) - return util.DelayedSignal(weight / 4 / np.pi * out, fs, t_offset + delay) + modal_response = _sig.sosfilt(sos[m], data)[:, _np.newaxis] + out += (2 * m + 1) * modal_response * _legendre(m, _np.cos(phaseshift)) + return _util.DelayedSignal(weight / 4 / _np.pi * out, fs, t_offset + delay) diff --git a/sfs/td/source.py b/sfs/td/source.py index 017aeb4..338dfdb 100644 --- a/sfs/td/source.py +++ b/sfs/td/source.py @@ -23,10 +23,10 @@ grid = sfs.util.xyz_grid([-2, 3], [-1, 2], 0, spacing=0.02) """ +import numpy as _np -import numpy as np -from .. import util -from .. import default +from .. import default as _default +from .. import util as _util def point(xs, signal, observation_time, grid, c=None): @@ -72,23 +72,23 @@ def point(xs, signal, observation_time, grid, c=None): sfs.plot2d.level(p, grid) """ - xs = util.asarray_1d(xs) - data, samplerate, signal_offset = util.as_delayed_signal(signal) - data = util.asarray_1d(data) - grid = util.as_xyz_components(grid) + xs = _util.asarray_1d(xs) + data, samplerate, signal_offset = _util.as_delayed_signal(signal) + data = _util.asarray_1d(data) + grid = _util.as_xyz_components(grid) if c is None: - c = default.c - r = np.linalg.norm(grid - xs) + c = _default.c + r = _np.linalg.norm(grid - xs) # If r is +-0, the sound pressure is +-infinity - with np.errstate(divide='ignore'): - weights = 1 / (4 * np.pi * r) + with _np.errstate(divide='ignore'): + weights = 1 / (4 * _np.pi * r) delays = r / c base_time = observation_time - signal_offset - points_at_time = np.interp(base_time - delays, - np.arange(len(data)) / samplerate, + points_at_time = _np.interp(base_time - delays, + _np.arange(len(data)) / samplerate, data, left=0, right=0) # weights can be +-infinity - with np.errstate(invalid='ignore'): + with _np.errstate(invalid='ignore'): return weights * points_at_time @@ -139,10 +139,10 @@ def point_image_sources(x0, signal, observation_time, grid, L, max_order, """ if coeffs is None: - coeffs = np.ones(6) + coeffs = _np.ones(6) - positions, order = util.image_sources_for_box(x0, L, max_order) - source_strengths = np.prod(coeffs**order, axis=1) + positions, order = _util.image_sources_for_box(x0, L, max_order) + source_strengths = _np.prod(coeffs**order, axis=1) p = 0 for position, strength in zip(positions, source_strengths): diff --git a/sfs/td/wfs.py b/sfs/td/wfs.py index 36e95fd..ab2531f 100644 --- a/sfs/td/wfs.py +++ b/sfs/td/wfs.py @@ -42,11 +42,13 @@ def plot(d, selection, secondary_source, t=0): sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15) """ -import numpy as np -from numpy.core.umath_tests import inner1d # element-wise inner product -from .. import default -from .. import util -from . import secondary_source_point, apply_delays +import numpy as _np +from numpy.core.umath_tests import inner1d as _inner1d + +from . import apply_delays as _apply_delays +from . import secondary_source_point as _secondary_source_point +from .. import default as _default +from .. import util as _util def plane_25d(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): @@ -110,16 +112,16 @@ def plane_25d(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): """ if c is None: - c = default.c - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - n = util.normalize_vector(n) - xref = util.asarray_1d(xref) - g0 = np.sqrt(2 * np.pi * np.linalg.norm(xref - x0, axis=1)) - delays = inner1d(n, x0) / c - weights = 2 * g0 * inner1d(n, n0) - selection = util.source_selection_plane(n0, n) - return delays, weights, selection, secondary_source_point(c) + c = _default.c + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + n = _util.normalize_vector(n) + xref = _util.asarray_1d(xref) + g0 = _np.sqrt(2 * _np.pi * _np.linalg.norm(xref - x0, axis=1)) + delays = _inner1d(n, x0) / c + weights = 2 * g0 * _inner1d(n, n0) + selection = _util.source_selection_plane(n0, n) + return delays, weights, selection, _secondary_source_point(c) def point_25d(x0, n0, xs, xref=[0, 0, 0], c=None): @@ -185,18 +187,18 @@ def point_25d(x0, n0, xs, xref=[0, 0, 0], c=None): """ if c is None: - c = default.c - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - xref = util.asarray_1d(xref) - g0 = np.sqrt(2 * np.pi * np.linalg.norm(xref - x0, axis=1)) + c = _default.c + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + xs = _util.asarray_1d(xs) + xref = _util.asarray_1d(xref) + g0 = _np.sqrt(2 * _np.pi * _np.linalg.norm(xref - x0, axis=1)) ds = x0 - xs - r = np.linalg.norm(ds, axis=1) + r = _np.linalg.norm(ds, axis=1) delays = r/c - weights = g0 * inner1d(ds, n0) / (2 * np.pi * r**(3/2)) - selection = util.source_selection_point(n0, x0, xs) - return delays, weights, selection, secondary_source_point(c) + weights = g0 * _inner1d(ds, n0) / (2 * _np.pi * r**(3/2)) + selection = _util.source_selection_point(n0, x0, xs) + return delays, weights, selection, _secondary_source_point(c) def focused_25d(x0, n0, xs, ns, xref=[0, 0, 0], c=None): @@ -267,19 +269,19 @@ def focused_25d(x0, n0, xs, ns, xref=[0, 0, 0], c=None): """ if c is None: - c = default.c - x0 = util.asarray_of_rows(x0) - n0 = util.asarray_of_rows(n0) - xs = util.asarray_1d(xs) - xref = util.asarray_1d(xref) + c = _default.c + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + xs = _util.asarray_1d(xs) + xref = _util.asarray_1d(xref) ds = x0 - xs - r = np.linalg.norm(ds, axis=1) - g0 = np.sqrt(np.linalg.norm(xref - x0, axis=1) - / (np.linalg.norm(xref - x0, axis=1) + r)) + r = _np.linalg.norm(ds, axis=1) + g0 = _np.sqrt(_np.linalg.norm(xref - x0, axis=1) + / (_np.linalg.norm(xref - x0, axis=1) + r)) delays = -r/c - weights = g0 * inner1d(ds, n0) / (2 * np.pi * r**(3/2)) - selection = util.source_selection_focused(ns, x0, xs) - return delays, weights, selection, secondary_source_point(c) + weights = g0 * _inner1d(ds, n0) / (2 * _np.pi * r**(3/2)) + selection = _util.source_selection_focused(ns, x0, xs) + return delays, weights, selection, _secondary_source_point(c) def driving_signals(delays, weights, signal): @@ -306,7 +308,7 @@ def driving_signals(delays, weights, signal): and a (possibly negative) time offset (in seconds). """ - delays = util.asarray_1d(delays) - weights = util.asarray_1d(weights) - data, samplerate, signal_offset = apply_delays(signal, delays) - return util.DelayedSignal(data * weights, samplerate, signal_offset) + delays = _util.asarray_1d(delays) + weights = _util.asarray_1d(weights) + data, samplerate, signal_offset = _apply_delays(signal, delays) + return _util.DelayedSignal(data * weights, samplerate, signal_offset) From 4f5e4d3a7409b1033fe86b1bb6901bb2b6760a84 Mon Sep 17 00:00:00 2001 From: Sascha Spors Date: Fri, 30 Aug 2019 11:39:40 +0200 Subject: [PATCH 078/101] Fixed secondary source type in 2D HOA driving function --- sfs/fd/nfchoa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sfs/fd/nfchoa.py b/sfs/fd/nfchoa.py index e326a43..862f826 100644 --- a/sfs/fd/nfchoa.py +++ b/sfs/fd/nfchoa.py @@ -98,7 +98,7 @@ def plane_2d(omega, x0, r0, n=[0, 1, 0], *, max_order=None, c=None): for m in range(-max_order, max_order + 1): d += 1j**-m / _hankel2(m, k * r0) * _np.exp(1j * m * (phi0 - phi)) selection = _util.source_selection_all(len(x0)) - return -2j / (_np.pi*r0) * d, selection, _secondary_source_point(omega, c) + return -2j / (_np.pi*r0) * d, selection, _secondary_source_line(omega, c) def point_25d(omega, x0, r0, xs, *, max_order=None, c=None): From 3453e987ee742bd1c4af42af0c405df8363e6a4d Mon Sep 17 00:00:00 2001 From: Sascha Spors Date: Fri, 30 Aug 2019 11:42:12 +0200 Subject: [PATCH 079/101] Fixed missing import --- sfs/fd/nfchoa.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sfs/fd/nfchoa.py b/sfs/fd/nfchoa.py index 862f826..d41c2b7 100644 --- a/sfs/fd/nfchoa.py +++ b/sfs/fd/nfchoa.py @@ -32,6 +32,7 @@ def plot(d, selection, secondary_source): from scipy.special import hankel2 as _hankel2 from . import secondary_source_point as _secondary_source_point +from . import secondary_source_line as _secondary_source_line from .. import util as _util From b7cdac53bbb778a370d1f5ddefe2f525b0eb3657 Mon Sep 17 00:00:00 2001 From: Frank Schultz Date: Wed, 4 Sep 2019 14:23:53 +0200 Subject: [PATCH 080/101] Update source.py fix calling for line source arguments --- sfs/fd/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sfs/fd/source.py b/sfs/fd/source.py index 530ecc4..5d59564 100644 --- a/sfs/fd/source.py +++ b/sfs/fd/source.py @@ -570,7 +570,7 @@ def line_dirichlet_edge(omega, x0, grid, *, alpha=_np.pi*3/2, Nc=None, c=None): p = p * -1j * _np.pi / alpha - pl = line(omega, x0, None, grid, c=c) + pl = line(omega, x0, grid, c=c) p[~idxa] = pl[~idxa] return p From c89f53d08a1f631e41dfe39f6cafe57c8ca48055 Mon Sep 17 00:00:00 2001 From: Sascha Spors Date: Wed, 4 Sep 2019 14:45:40 +0200 Subject: [PATCH 081/101] =?UTF-8?q?Computation=20of=20spatially=20bandlimi?= =?UTF-8?q?ted=20line=20source,=20fixed=20missing=20docum=E2=80=A6=20(#155?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added spatially (modal) bandlimited line source * fixed line_dirichlet_edge() to handle grids with different dimensions Co-authored-by: fs446 --- sfs/fd/source.py | 115 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 3 deletions(-) diff --git a/sfs/fd/source.py b/sfs/fd/source.py index 5d59564..6a0b860 100644 --- a/sfs/fd/source.py +++ b/sfs/fd/source.py @@ -409,7 +409,22 @@ def point_image_sources(omega, x0, grid, L, *, max_order, coeffs=None, c=None): def line(omega, x0, grid, *, c=None): r"""Line source parallel to the z-axis. - Note: third component of x0 is ignored. + Parameters + ---------- + omega : float + Frequency of source. + x0 : (3,) array_like + Position of source. Note: third component of x0 is ignored. + grid : triple of array_like + The grid that is used for the sound field calculations. + See `sfs.util.xyz_grid()`. + c : float, optional + Speed of sound. + + Returns + ------- + numpy.ndarray + Sound pressure at positions given by *grid*. Notes ----- @@ -448,6 +463,18 @@ def line(omega, x0, grid, *, c=None): def line_velocity(omega, x0, grid, *, c=None, rho0=None): """Velocity of line source parallel to the z-axis. + Parameters + ---------- + omega : float + Frequency of source. + x0 : (3,) array_like + Position of source. Note: third component of x0 is ignored. + grid : triple of array_like + The grid that is used for the sound field calculations. + See `sfs.util.xyz_grid()`. + c : float, optional + Speed of sound. + Returns ------- `XyzComponents` @@ -490,7 +517,19 @@ def line_velocity(omega, x0, grid, *, c=None, rho0=None): def line_dipole(omega, x0, n0, grid, *, c=None): r"""Line source with dipole characteristics parallel to the z-axis. - Note: third component of x0 is ignored. + Parameters + ---------- + omega : float + Frequency of source. + x0 : (3,) array_like + Position of source. Note: third component of x0 is ignored. + x0 : (3,) array_like + Normal vector of the source. + grid : triple of array_like + The grid that is used for the sound field calculations. + See `sfs.util.xyz_grid()`. + c : float, optional + Speed of sound. Notes ----- @@ -510,6 +549,76 @@ def line_dipole(omega, x0, n0, grid, *, c=None): return _duplicate_zdirection(p, grid) +def line_bandlimited(omega, x0, grid, *, max_order=None, c=None): + r"""Spatially bandlimited (modal) line source parallel to the z-axis. + + Parameters + ---------- + omega : float + Frequency of source. + x0 : (3,) array_like + Position of source. Note: third component of x0 is ignored. + grid : triple of array_like + The grid that is used for the sound field calculations. + See `sfs.util.xyz_grid()`. + max_order : int, optional + Number of elements for series expansion of the source. + No bandlimitation if not given. + c : float, optional + Speed of sound. + + Returns + ------- + numpy.ndarray + Sound pressure at positions given by *grid*. + + Notes + ----- + .. math:: + + G(\x-\x_0,\w) = -\frac{\i}{4} \sum_{\nu = - N}^{N} + e^{j \nu (\alpha - \alpha_0)} + \begin{cases} + J_\nu(\frac{\omega}{c} r) H_\nu^\text{(2)}(\frac{\omega}{c} r_0) + & \text{for } r \leq r_0 \\ + J_\nu(\frac{\omega}{c} r_0) H_\nu^\text{(2)}(\frac{\omega}{c} r) + & \text{for } r > r_0 \\ + \end{cases} + + Examples + -------- + .. plot:: + :context: close-figs + + p = sfs.fd.source.line_bandlimited(omega, x0, grid, max_order=10) + sfs.plot2d.amplitude(p * normalization_line, grid) + plt.title("Bandlimited Line Source at {} m".format(x0[:2])) + + + """ + k = _util.wavenumber(omega, c) + x0 = _util.asarray_1d(x0)[:2] # ignore z-components + r0 = _np.linalg.norm(x0) + phi0 = _np.arctan2(x0[1], x0[0]) + + grid = _util.as_xyz_components(grid) + r = _np.linalg.norm(grid[:2]) + phi = _np.arctan2(grid[1], grid[0]) + + if max_order is None: + max_order = int(_np.ceil(k * _np.max(r))) + + p = _np.zeros((grid[1].shape[0], grid[0].shape[1]), dtype=complex) + idxr = (r <= r0) + for m in range(-max_order, max_order + 1): + p[idxr] -= 1j/4 * _special.hankel2(m, k * r0) * \ + _special.jn(m, k * r[idxr]) * _np.exp(1j * m * (phi[idxr] - phi0)) + p[~idxr] -= 1j/4 * _special.hankel2(m, k * r[~idxr]) * \ + _special.jn(m, k * r0) * _np.exp(1j * m * (phi[~idxr] - phi0)) + + return _duplicate_zdirection(p, grid) + + def line_dirichlet_edge(omega, x0, grid, *, alpha=_np.pi*3/2, Nc=None, c=None): """Line source scattered at an edge with Dirichlet boundary conditions. @@ -557,7 +666,7 @@ def line_dirichlet_edge(omega, x0, grid, *, alpha=_np.pi*3/2, Nc=None, c=None): epsilon = _np.ones(Nc) # weights for series expansion epsilon[0] = 2 - p = _np.zeros((grid[0].shape[1], grid[1].shape[0]), dtype=complex) + p = _np.zeros((grid[1].shape[0], grid[0].shape[1]), dtype=complex) idxr = (r <= r_s) idxa = (phi <= alpha) for m in _np.arange(Nc): From d74ebe4dfc0b571e6a56a41748521e2a2747f754 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 16 Dec 2019 19:04:04 +0100 Subject: [PATCH 082/101] Travis-CI: Make using python3 explicit --- .travis.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 113bf3b..85c09b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,13 +11,15 @@ addons: - pandoc - ffmpeg install: - - pip install . - - pip install -r tests/requirements.txt - - pip install -r doc/requirements.txt + - python3 --version + - python3 -m pip --version + - python3 -m pip install . + - python3 -m pip install -r tests/requirements.txt + - python3 -m pip install -r doc/requirements.txt # This is needed in example scripts: - - pip install pillow + - python3 -m pip install pillow script: - - python -m pytest - - python doc/examples/run_all.py + - python3 -m pytest + - python3 doc/examples/run_all.py # This executes the example notebooks and runs the doctests: - - python -m sphinx doc/ _build/ -b doctest + - python3 -m sphinx doc/ _build/ -b doctest From 45f2c28794c83b42b778be6ea77243c80d3615b7 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 16 Dec 2019 18:08:42 +0100 Subject: [PATCH 083/101] Travis-CI: Add test on macOS --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 85c09b8..bf641f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,18 @@ matrix: - python: "3.5" - python: "3.6" - python: "3.7" + - os: osx + language: generic + python: "" addons: apt: packages: - pandoc - ffmpeg + homebrew: + packages: + - pandoc + - ffmpeg install: - python3 --version - python3 -m pip --version From f11970f582dab527ada81d33f70340535ca3e90d Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 16 Dec 2019 18:03:21 +0100 Subject: [PATCH 084/101] Travis-CI: Add Python 3.8 to test matrix --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index bf641f3..03c40e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ matrix: - python: "3.5" - python: "3.6" - python: "3.7" + - python: "3.8" - os: osx language: generic python: "" From cdb7b347e6d724fcd327744108c2dd9976455b1a Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 16 Dec 2019 18:04:39 +0100 Subject: [PATCH 085/101] Travis-CI: Upgrade to Ubuntu Bionic Beaver --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 03c40e9..11dc995 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: python -dist: xenial +dist: bionic matrix: include: - python: "3.5" From 84de59f6d920e6b697378649b882a860a4e6361f Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sun, 22 Dec 2019 11:52:54 +0100 Subject: [PATCH 086/101] DOC: autodoc_default_flags -> autodoc_default_options Former has been deprecated and it stopped working in Sphinx's master branch: https://github.com/sphinx-doc/sphinx/issues/6930 --- doc/conf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index a319efa..07b7c4f 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -56,7 +56,10 @@ autoclass_content = 'init' autodoc_member_order = 'bysource' -autodoc_default_flags = ['members', 'undoc-members'] +autodoc_default_options = { + 'members': True, + 'undoc-members': True, +} autosummary_generate = ['api'] From 2bc70e99edc7479cb924cc8eedca8c34136cd639 Mon Sep 17 00:00:00 2001 From: Sascha Spors Date: Mon, 24 Feb 2020 16:17:42 +0100 Subject: [PATCH 087/101] Consistent nomenclature for secondary source distance --- sfs/array.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/sfs/array.py b/sfs/array.py index 899075c..1727475 100644 --- a/sfs/array.py +++ b/sfs/array.py @@ -289,7 +289,7 @@ def rectangular(N, spacing, *, center=[0, 0, 0], orientation=[1, 0, 0]): return SecondarySourceDistribution(positions, normals, weights) -def rounded_edge(Nxy, Nr, dx, *, center=[0, 0, 0], orientation=[1, 0, 0]): +def rounded_edge(Nxy, Nr, spacing, *, center=[0, 0, 0], orientation=[1, 0, 0]): """Return SSD along the xy-axis with rounded edge at the origin. Parameters @@ -299,6 +299,8 @@ def rounded_edge(Nxy, Nr, dx, *, center=[0, 0, 0], orientation=[1, 0, 0]): Nr : int Number of secondary sources in rounded edge. Radius of edge is adjusted to equdistant sampling along entire array. + spacing : float + Distance (in metres) between secondary sources. center : (3,) array_like, optional Position of edge. orientation : (3,) array_like, optional @@ -323,10 +325,11 @@ def rounded_edge(Nxy, Nr, dx, *, center=[0, 0, 0], orientation=[1, 0, 0]): """ # radius of rounded edge Nr += 1 - R = 2/_np.pi * Nr * dx + R = 2/_np.pi * Nr * spacing # array along y-axis - x00, n00, a00 = linear(Nxy, dx, center=[0, Nxy//2*dx+dx/2+R, 0]) + x00, n00, a00 = linear(Nxy, spacing, + center=[0, Nxy//2*spacing+spacing/2+R, 0]) x00 = _np.flipud(x00) positions = x00 directions = n00 @@ -342,13 +345,14 @@ def rounded_edge(Nxy, Nr, dx, *, center=[0, 0, 0], orientation=[1, 0, 0]): x00[n, 1] = R * (1 - _np.sin(alpha)) n00[n, 0] = _np.cos(alpha) n00[n, 1] = _np.sin(alpha) - a00[n] = dx + a00[n] = spacing positions = _np.concatenate((positions, x00)) directions = _np.concatenate((directions, n00)) weights = _np.concatenate((weights, a00)) # array along x-axis - x00, n00, a00 = linear(Nxy, dx, center=[Nxy//2*dx-dx/2+R, 0, 0], + x00, n00, a00 = linear(Nxy, spacing, + center=[Nxy//2*spacing-spacing/2+R, 0, 0], orientation=[0, 1, 0]) x00 = _np.flipud(x00) positions = _np.concatenate((positions, x00)) @@ -363,13 +367,15 @@ def rounded_edge(Nxy, Nr, dx, *, center=[0, 0, 0], orientation=[1, 0, 0]): return SecondarySourceDistribution(positions, directions, weights) -def edge(Nxy, dx, *, center=[0, 0, 0], orientation=[1, 0, 0]): +def edge(Nxy, spacing, *, center=[0, 0, 0], orientation=[1, 0, 0]): """Return SSD along the xy-axis with sharp edge at the origin. Parameters ---------- Nxy : int Number of secondary sources along x- and y-axis. + spacing : float + Distance (in metres) between secondary sources. center : (3,) array_like, optional Position of edge. orientation : (3,) array_like, optional @@ -393,14 +399,16 @@ def edge(Nxy, dx, *, center=[0, 0, 0], orientation=[1, 0, 0]): """ # array along y-axis - x00, n00, a00 = linear(Nxy, dx, center=[0, Nxy//2*dx+dx/2, 0]) + x00, n00, a00 = linear(Nxy, spacing, + center=[0, Nxy//2*spacing+spacing/2, 0]) x00 = _np.flipud(x00) positions = x00 directions = n00 weights = a00 # array along x-axis - x00, n00, a00 = linear(Nxy, dx, center=[Nxy//2*dx-dx/2, 0, 0], + x00, n00, a00 = linear(Nxy, spacing, + center=[Nxy//2*spacing-spacing/2, 0, 0], orientation=[0, 1, 0]) x00 = _np.flipud(x00) positions = _np.concatenate((positions, x00)) From 4d55ff05a1d77438ae914ab3b7b5370bfa9dda98 Mon Sep 17 00:00:00 2001 From: Sascha Spors Date: Thu, 5 Mar 2020 16:39:33 +0100 Subject: [PATCH 088/101] Added optional parameter for size and documentation --- sfs/plot2d.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/sfs/plot2d.py b/sfs/plot2d.py index f05936e..ae2a348 100644 --- a/sfs/plot2d.py +++ b/sfs/plot2d.py @@ -72,8 +72,20 @@ def reference(xref, *, size=0.1, ax=None): ax.plot((xref[0]-size, xref[0]+size), (xref[1]+size, xref[1]-size), 'k-') -def secondary_sources(x0, n0, *, grid=None): - """Simple plot of secondary source locations.""" +def secondary_sources(x0, n0, *, size=0.05, grid=None): + """Simple visualization of secondary source locations. + + Parameters + ---------- + x0 : (N, 3) array_like + Loudspeaker positions. + n0 : (N, 3) or (3,) array_like + Normal vector(s) of loudspeakers. + size : float, optional + Size of loudspeakers in metres. + grid : triple of array_like, optional + If specified, only loudspeakers within the *grid* are shown. + """ x0 = _np.asarray(x0) n0 = _np.asarray(n0) ax = _plt.gca() @@ -84,7 +96,7 @@ def secondary_sources(x0, n0, *, grid=None): # plot symbols for x00 in x0: - ss = _plt.Circle(x00[0:2], .05, edgecolor='k', facecolor='k') + ss = _plt.Circle(x00[0:2], size, edgecolor='k', facecolor='k') ax.add_artist(ss) From 21553ec9a1fbeddc766bd114c2789137123f7c08 Mon Sep 17 00:00:00 2001 From: Frank Schultz Date: Fri, 25 Sep 2020 10:56:47 +0200 Subject: [PATCH 089/101] bugfix: xref same dim as x0 to create reference curves not only a single point --- sfs/fd/wfs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sfs/fd/wfs.py b/sfs/fd/wfs.py index 817b4eb..e134227 100644 --- a/sfs/fd/wfs.py +++ b/sfs/fd/wfs.py @@ -223,7 +223,7 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): x0 = _util.asarray_of_rows(x0) n0 = _util.asarray_of_rows(n0) xs = _util.asarray_1d(xs) - xref = _util.asarray_1d(xref) + xref = _util.asarray_of_rows(xref) k = _util.wavenumber(omega, c) ds = x0 - xs From bc0eb0fe9fa39e9e9d29c6e579f3677f0237cc23 Mon Sep 17 00:00:00 2001 From: Sascha Spors Date: Wed, 11 Nov 2020 13:34:28 +0100 Subject: [PATCH 090/101] API changes in matplotlib.pyplot.scatter, added docstring --- sfs/plot2d.py | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/sfs/plot2d.py b/sfs/plot2d.py index ae2a348..efadb9c 100644 --- a/sfs/plot2d.py +++ b/sfs/plot2d.py @@ -330,8 +330,38 @@ def level(p, grid, *, xnorm=None, power=False, cmap=None, vmax=3, vmin=-50, def particles(x, *, trim=None, ax=None, xlabel='x (m)', ylabel='y (m)', - edgecolor='', marker='.', s=15, **kwargs): - """Plot particle positions as scatter plot""" + edgecolors=None, marker='.', s=15, **kwargs): + """Plot particle positions as scatter plot. + + Parameters + ---------- + x : triple or pair of array_like + x, y and optionally z components of particle positions. The z + components are ignored. + If the values are complex, the imaginary parts are ignored. + + Returns + ------- + Scatter + See :func:`matplotlib.pyplot.scatter`. + + Other Parameters + ---------------- + trim : array of float, optional + xmin, xmax, ymin, ymax limits for which the particles are plotted. + ax : Axes, optional + If given, the plot is created on *ax* instead of the current + axis (see :func:`matplotlib.pyplot.gca`). + xlabel, ylabel : str + Overwrite default x/y labels. Use ``xlabel=''`` and + ``ylabel=''`` to remove x/y labels. The labels can be changed + afterwards with :func:`matplotlib.pyplot.xlabel` and + :func:`matplotlib.pyplot.ylabel`. + edgecolors, markr, s, **kwargs + All further parameters are forwarded to + :func:`matplotlib.pyplot.scatter`. + + """ XX, YY = [_np.real(c) for c in x[:2]] if trim is not None: @@ -348,7 +378,7 @@ def particles(x, *, trim=None, ax=None, xlabel='x (m)', ylabel='y (m)', ax.set_xlabel(xlabel) if ylabel: ax.set_ylabel(ylabel) - return ax.scatter(XX, YY, edgecolor=edgecolor, marker=marker, s=s, + return ax.scatter(XX, YY, edgecolors=edgecolors, marker=marker, s=s, **kwargs) From 2622d7099833aa21a0817a2053948b4dd4c8ca4e Mon Sep 17 00:00:00 2001 From: Hagen Wierstorf Date: Tue, 1 Dec 2020 11:03:58 +0100 Subject: [PATCH 091/101] Replace travis with GitHub for running CI jobs (#166) * use Github Action for executing CI jobs * removed travis CI jobs * removed support for Python 3.5 --- .github/workflows/test.yml | 45 ++++++++++++++++++++++++++++++++++++++ .travis.yml | 33 ---------------------------- setup.py | 3 +-- 3 files changed, 46 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..22d7f47 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,45 @@ +name: Test + +on: [push, pull_request] + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] + python-version: [3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Prepare Ubuntu + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y pandoc ffmpeg + if: matrix.os == 'ubuntu-latest' + - name: Prepare OSX + run: brew install pandoc ffmpeg + if: matrix.os == 'macOS-latest' + - name: prepare Windows + run: choco install pandoc ffmpeg + if: matrix.os == 'windows-latest' + - name: Install dependencies + run: | + python -V + python -m pip install --upgrade pip + python -m pip install . + python -m pip install -r tests/requirements.txt + python -m pip install -r doc/requirements.txt + # This is needed in example scripts: + python -m pip install pillow + - name: Test + run: python -m pytest + - name: Test examples + run: python doc/examples/run_all.py + - name: Test documentation + run: python -m sphinx doc/ _build/ -b doctest diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 11dc995..0000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -language: python -dist: bionic -matrix: - include: - - python: "3.5" - - python: "3.6" - - python: "3.7" - - python: "3.8" - - os: osx - language: generic - python: "" -addons: - apt: - packages: - - pandoc - - ffmpeg - homebrew: - packages: - - pandoc - - ffmpeg -install: - - python3 --version - - python3 -m pip --version - - python3 -m pip install . - - python3 -m pip install -r tests/requirements.txt - - python3 -m pip install -r doc/requirements.txt - # This is needed in example scripts: - - python3 -m pip install pillow -script: - - python3 -m pytest - - python3 doc/examples/run_all.py - # This executes the example notebooks and runs the doctests: - - python3 -m sphinx doc/ _build/ -b doctest diff --git a/setup.py b/setup.py index fc2bee5..9a7d870 100644 --- a/setup.py +++ b/setup.py @@ -24,14 +24,13 @@ keywords="audio SFS WFS Ambisonics".split(), url="http://github.com/sfstoolbox/", platforms='any', - python_requires='>=3.5', + python_requires='>=3.6', classifiers=[ "Development Status :: 3 - Alpha", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3 :: Only", From 2391e1edeeab522abb62122692b34021e6f94045 Mon Sep 17 00:00:00 2001 From: Sascha Spors Date: Tue, 1 Dec 2020 12:50:22 +0100 Subject: [PATCH 092/101] Release 0.6.0 (#167) --- NEWS.rst | 5 +++++ sfs/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index 6df799c..be8f405 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,6 +1,11 @@ Version History =============== + +Version 0.6.0 (2020-12-01): + * New function `sfs.fd.source.line_bandlimited()` computing the sound field of a spatially bandlimited line source + * Drop support for Python 3.5 + Version 0.5.0 (2019-03-18): * Switching to separate `sfs.plot2d` and `sfs.plot3d` for plotting functions * Move `sfs.util.displacement()` to `sfs.fd.displacement()` diff --git a/sfs/__init__.py b/sfs/__init__.py index 2072d81..85d02d7 100644 --- a/sfs/__init__.py +++ b/sfs/__init__.py @@ -16,7 +16,7 @@ util """ -__version__ = "0.5.0" +__version__ = "0.6.0" class default: From 17ab94d0d349daf1539f7b320e542aab0568d595 Mon Sep 17 00:00:00 2001 From: Frank Schultz Date: Fri, 4 Dec 2020 09:37:18 +0100 Subject: [PATCH 093/101] Fix doc string of synthesize() (#165) --- sfs/fd/__init__.py | 3 +-- sfs/td/__init__.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/sfs/fd/__init__.py b/sfs/fd/__init__.py index 2d9555e..1dfe2cb 100644 --- a/sfs/fd/__init__.py +++ b/sfs/fd/__init__.py @@ -53,8 +53,7 @@ def synthesize(d, weights, ssd, secondary_source_function, **kwargs): This signature is expected:: secondary_source_function( - position, normal_vector, weight, driving_function_weight, - **kwargs) -> numpy.ndarray + position, normal_vector, **kwargs) -> numpy.ndarray **kwargs All keyword arguments are forwarded to *secondary_source_function*. diff --git a/sfs/td/__init__.py b/sfs/td/__init__.py index 5efb20a..a786ea5 100644 --- a/sfs/td/__init__.py +++ b/sfs/td/__init__.py @@ -37,8 +37,7 @@ def synthesize(signals, weights, ssd, secondary_source_function, **kwargs): This signature is expected:: secondary_source_function( - position, normal_vector, weight, driving_signal, - **kwargs) -> numpy.ndarray + position, normal_vector, **kwargs) -> numpy.ndarray **kwargs All keyword arguments are forwarded to *secondary_source_function*. From 002a967c92d1ca6fc904ffdefffcebaa10c49748 Mon Sep 17 00:00:00 2001 From: Hagen Wierstorf Date: Mon, 18 Jan 2021 17:18:23 +0100 Subject: [PATCH 094/101] DOC: add compatibility for sphinxcontrib-bibtex>=2 --- doc/conf.py | 2 ++ doc/references.rst | 2 +- doc/requirements.txt | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 07b7c4f..caec59a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -46,6 +46,8 @@ 'nbsphinx', ] +bibtex_bibfiles = ['references.bib'] + nbsphinx_execute_arguments = [ "--InlineBackend.figure_formats={'svg', 'pdf'}", "--InlineBackend.rc={'figure.dpi': 96}", diff --git a/doc/references.rst b/doc/references.rst index 4b3db95..d08e90e 100644 --- a/doc/references.rst +++ b/doc/references.rst @@ -1,6 +1,6 @@ References ========== -.. bibliography:: references.bib +.. bibliography:: :style: alpha :all: diff --git a/doc/requirements.txt b/doc/requirements.txt index e2e3ce9..4f038ca 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -2,7 +2,7 @@ Sphinx>=1.3.6 Sphinx-RTD-Theme nbsphinx ipykernel -sphinxcontrib-bibtex +sphinxcontrib-bibtex>=2.1.4 NumPy SciPy From 230e2e21901d802d8e52f7ef4a800c2e955c0eb1 Mon Sep 17 00:00:00 2001 From: Hagen Wierstorf Date: Tue, 19 Jan 2021 13:32:14 +0100 Subject: [PATCH 095/101] Fix reference in max_order_circular_harmonics() --- sfs/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sfs/util.py b/sfs/util.py index b054346..c15358f 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -610,7 +610,7 @@ def max_order_circular_harmonics(N): r"""Maximum order of 2D/2.5D HOA. It returns the maximum order for which no spatial aliasing appears. - It is given on page 132 of [Ahrens2012]_ as + It is given on page 132 of :cite:`Ahrens2012` as .. math:: \mathtt{max\_order} = From 21bd666e9babb09df10dac0228b0fb077eee82ff Mon Sep 17 00:00:00 2001 From: Hagen Wierstorf Date: Tue, 19 Jan 2021 13:35:57 +0100 Subject: [PATCH 096/101] DOC: include only cited references --- doc/references.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/references.rst b/doc/references.rst index d08e90e..8905446 100644 --- a/doc/references.rst +++ b/doc/references.rst @@ -3,4 +3,3 @@ References .. bibliography:: :style: alpha - :all: From efdda38a9fb44d5a529c1deec56b27e44f5f785c Mon Sep 17 00:00:00 2001 From: Frank Schultz Date: Sat, 16 Jan 2021 16:28:44 +0100 Subject: [PATCH 097/101] introduce new wfs 2.5D point source time domain driving function --- sfs/td/wfs.py | 117 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 109 insertions(+), 8 deletions(-) diff --git a/sfs/td/wfs.py b/sfs/td/wfs.py index ab2531f..d0aa1f4 100644 --- a/sfs/td/wfs.py +++ b/sfs/td/wfs.py @@ -39,7 +39,8 @@ def plot(d, selection, secondary_source, t=0): p = sfs.td.synthesize(d, selection, array, secondary_source, grid=grid, observation_time=t) sfs.plot2d.level(p, grid) - sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15) + sfs.plot2d.loudspeakers(array.x, array.n, + selection * array.a, size=0.15) """ import numpy as _np @@ -92,9 +93,9 @@ def plane_25d(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): .. math:: - d_{2.5D}(x_0,t) = h(t) + d_{2.5D}(x_0,t) = 2 g_0 \scalarprod{n}{n_0} - \dirac{t - \frac{1}{c} \scalarprod{n}{x_0}} + \dirac{t - \frac{1}{c} \scalarprod{n}{x_0}} \ast_t h(t) with wfs(2.5D) prefilter h(t), which is not implemented yet. @@ -125,7 +126,101 @@ def plane_25d(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): def point_25d(x0, n0, xs, xref=[0, 0, 0], c=None): - r"""Point source by 2.5-dimensional WFS. + r"""Driving function for 2.5-dimensional WFS of a virtual point source. + + .. versionchanged:: 0.61 + see notes, old handling of `point_25d()` is now `point_25d_legacy()` + + Parameters + ---------- + x0 : (N, 3) array_like + Sequence of secondary source positions. + n0 : (N, 3) array_like + Sequence of secondary source orientations. + xs : (3,) array_like + Virtual source position. + xref : (N, 3) array_like or (3,) array_like + Reference curve of correct amplitude xref(x0) + c : float, optional + Speed of sound + + Returns + ------- + delays : (N,) numpy.ndarray + Delays of secondary sources in seconds. + weights: (N,) numpy.ndarray + Weights of secondary sources. + selection : (N,) numpy.ndarray + Boolean array containing ``True`` or ``False`` depending on + whether the corresponding secondary source is "active" or not. + secondary_source_function : callable + A function that can be used to create the sound field of a + single secondary source. See `sfs.td.synthesize()`. + + Notes + ----- + + Eq. (2.138) in :cite:`Schultz2016`: + + .. math:: + + d_{2.5D}(x_0, x_{ref}, t) = + \sqrt{8\pi} + \frac{\scalarprod{(x_0 - x_s)}{n_0}}{|x_0 - x_s|} + \sqrt{\frac{|x_0 - x_s||x_0 - x_{ref}|}{|x_0 - x_s|+|x_0 - x_{ref}|}} + \cdot + \frac{\dirac{t - \frac{|x_0 - x_s|}{c}}}{4\pi |x_0 - x_s|} \ast_t h(t) + + .. math:: + + h(t) = F^{-1}(\sqrt{\frac{j \omega}{c}}) + + with wfs(2.5D) prefilter h(t), which is not implemented yet. + + `point_25d()` derives WFS from 3D to 2.5D via the stationary phase + approximation approach (i.e. the Delft approach). + The theoretical link of `point_25d()` and `point_25d_legacy()` was + introduced as *unified WFS framework* in :cite:`Firtha2017`. + + Examples + -------- + .. plot:: + :context: close-figs + + delays, weights, selection, secondary_source = \ + sfs.td.wfs.point_25d(array.x, array.n, xs) + d = sfs.td.wfs.driving_signals(delays, weights, signal) + plot(d, selection, secondary_source, t=ts) + + """ + if c is None: + c = _default.c + x0 = _util.asarray_of_rows(x0) + n0 = _util.asarray_of_rows(n0) + xs = _util.asarray_1d(xs) + xref = _util.asarray_of_rows(xref) + + x0xs = x0 - xs + x0xref = x0 - xref + x0xs_n = _np.linalg.norm(x0xs, axis=1) + x0xref_n = _np.linalg.norm(x0xref, axis=1) + + g0 = 1/(_np.sqrt(2*_np.pi)*x0xs_n**2) + g0 *= _np.sqrt((x0xs_n*x0xref_n)/(x0xs_n+x0xref_n)) + + delays = x0xs_n/c + weights = g0*_inner1d(x0xs, n0) + selection = _util.source_selection_point(n0, x0, xs) + return delays, weights, selection, _secondary_source_point(c) + + +def point_25d_legacy(x0, n0, xs, xref=[0, 0, 0], c=None): + r"""Driving function for 2.5-dimensional WFS of a virtual point source. + + .. versionadded:: 0.61 + `point_25d()` was renamed to `point_25d_legacy()` (and a new + function with the name `point_25d()` was introduced). See notes below + for further details. Parameters ---------- @@ -166,15 +261,21 @@ def point_25d(x0, n0, xs, xref=[0, 0, 0], c=None): .. math:: - d_{2.5D}(x_0,t) = h(t) + d_{2.5D}(x_0,t) = \frac{g_0 \scalarprod{(x_0 - x_s)}{n_0}} {2\pi |x_0 - x_s|^{3/2}} - \dirac{t - \frac{|x_0 - x_s|}{c}} + \dirac{t - \frac{|x_0 - x_s|}{c}} \ast_t h(t) with wfs(2.5D) prefilter h(t), which is not implemented yet. See :sfs:`d_wfs/#equation-td-wfs-point-25d` + `point_25d_legacy()` derives 2.5D WFS from the 2D + Neumann-Rayleigh integral (i.e. the approach by Rabenstein & Spors), cf. + :cite:`Spors2008`. + The theoretical link of `point_25d()` and `point_25d_legacy()` was + introduced as *unified WFS framework* in :cite:`Firtha2017`. + Examples -------- .. plot:: @@ -248,10 +349,10 @@ def focused_25d(x0, n0, xs, ns, xref=[0, 0, 0], c=None): .. math:: - d_{2.5D}(x_0,t) = h(t) + d_{2.5D}(x_0,t) = \frac{g_0 \scalarprod{(x_0 - x_s)}{n_0}} {|x_0 - x_s|^{3/2}} - \dirac{t + \frac{|x_0 - x_s|}{c}} + \dirac{t + \frac{|x_0 - x_s|}{c}} \ast_t h(t) with wfs(2.5D) prefilter h(t), which is not implemented yet. From 834c69749ed7ae47487faf95dea3171511bc6a72 Mon Sep 17 00:00:00 2001 From: Frank Schultz Date: Sat, 5 Jun 2021 13:22:44 +0200 Subject: [PATCH 098/101] Release 0.6.1 --- NEWS.rst | 3 +++ sfs/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index be8f405..457f521 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -2,6 +2,9 @@ Version History =============== +Version 0.6.1 (2021-06-05): + * New default driving function for `sfs.td.wfs.point_25d()` for reference curve + Version 0.6.0 (2020-12-01): * New function `sfs.fd.source.line_bandlimited()` computing the sound field of a spatially bandlimited line source * Drop support for Python 3.5 diff --git a/sfs/__init__.py b/sfs/__init__.py index 85d02d7..2c6c16f 100644 --- a/sfs/__init__.py +++ b/sfs/__init__.py @@ -16,7 +16,7 @@ util """ -__version__ = "0.6.0" +__version__ = "0.6.1" class default: From 8c787f3bbaeae9fb06a2e75d57a2103badd83cf5 Mon Sep 17 00:00:00 2001 From: Frank Schultz Date: Sat, 5 Jun 2021 18:16:01 +0200 Subject: [PATCH 099/101] build doc fix, use sphinx4, mathjax2, html_css_files --- doc/conf.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index caec59a..35b2874 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -35,7 +35,6 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', - 'sphinx.ext.mathjax', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', # support for NumPy-style docstrings 'sphinx.ext.intersphinx', @@ -95,7 +94,22 @@ } plot_formats = ['svg', 'pdf'] -mathjax_config = { +# use mathjax2 with +# https://github.com/spatialaudio/nbsphinx/issues/572#issuecomment-853389268 +# and 'TeX' dictionary +# in future we might switch to mathjax3 once the +# 'begingroup' extension is available +# http://docs.mathjax.org/en/latest/input/tex/extensions/begingroup.html#begingroup +# https://mathjax.github.io/MathJax-demos-web/convert-configuration/convert-configuration.html +mathjax_path = ('https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js' + '?config=TeX-AMS-MML_HTMLorMML') +mathjax2_config = { + 'tex2jax': { + 'inlineMath': [['$', '$'], ['\\(', '\\)']], + 'processEscapes': True, + 'ignoreClass': 'document', + 'processClass': 'math|output_area', + }, 'TeX': { 'extensions': ['newcommand.js', 'begingroup.js'], # Support for \gdef }, @@ -219,9 +233,7 @@ # -- Options for HTML output ---------------------------------------------- -def setup(app): - """Include custom theme files to sphinx HTML header""" - app.add_stylesheet('css/title.css') +html_css_files = ['css/title.css'] # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. From 51d8493b39db63d01012688326d2a3fff95bf357 Mon Sep 17 00:00:00 2001 From: Frank Schultz Date: Sat, 5 Jun 2021 20:12:04 +0200 Subject: [PATCH 100/101] Release 0.6.2 --- NEWS.rst | 3 +++ sfs/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index 457f521..0a1d06d 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -2,6 +2,9 @@ Version History =============== +Version 0.6.2 (2021-06-05): + * build doc fix, use sphinx4, mathjax2, html_css_files + Version 0.6.1 (2021-06-05): * New default driving function for `sfs.td.wfs.point_25d()` for reference curve diff --git a/sfs/__init__.py b/sfs/__init__.py index 2c6c16f..6e3d57c 100644 --- a/sfs/__init__.py +++ b/sfs/__init__.py @@ -16,7 +16,7 @@ util """ -__version__ = "0.6.1" +__version__ = "0.6.2" class default: From 043e9dd0f6aae5eeebbed8357968a2669d29fc40 Mon Sep 17 00:00:00 2001 From: Frank Schultz Date: Thu, 21 Jan 2021 11:50:34 +0100 Subject: [PATCH 101/101] docstring mods wfs.py in td/fd follow up #168, reference to v0.6.1 --- sfs/fd/wfs.py | 5 ++--- sfs/td/wfs.py | 7 +++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/sfs/fd/wfs.py b/sfs/fd/wfs.py index e134227..44fb2c6 100644 --- a/sfs/fd/wfs.py +++ b/sfs/fd/wfs.py @@ -171,9 +171,8 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): Sequence of normal vectors of secondary sources. xs : (3,) array_like Position of virtual point source. - xref : (3,) array_like, optional - Reference point xref or contour xref(x0) for amplitude correct - synthesis. + xref : (N, 3) array_like or (3,) array_like + Contour xref(x0) for amplitude correct synthesis, reference point xref. c : float, optional Speed of sound in m/s. omalias: float, optional diff --git a/sfs/td/wfs.py b/sfs/td/wfs.py index d0aa1f4..3b59301 100644 --- a/sfs/td/wfs.py +++ b/sfs/td/wfs.py @@ -128,7 +128,7 @@ def plane_25d(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): def point_25d(x0, n0, xs, xref=[0, 0, 0], c=None): r"""Driving function for 2.5-dimensional WFS of a virtual point source. - .. versionchanged:: 0.61 + .. versionchanged:: 0.6.1 see notes, old handling of `point_25d()` is now `point_25d_legacy()` Parameters @@ -140,7 +140,7 @@ def point_25d(x0, n0, xs, xref=[0, 0, 0], c=None): xs : (3,) array_like Virtual source position. xref : (N, 3) array_like or (3,) array_like - Reference curve of correct amplitude xref(x0) + Contour xref(x0) for amplitude correct synthesis, reference point xref. c : float, optional Speed of sound @@ -159,7 +159,6 @@ def point_25d(x0, n0, xs, xref=[0, 0, 0], c=None): Notes ----- - Eq. (2.138) in :cite:`Schultz2016`: .. math:: @@ -217,7 +216,7 @@ def point_25d(x0, n0, xs, xref=[0, 0, 0], c=None): def point_25d_legacy(x0, n0, xs, xref=[0, 0, 0], c=None): r"""Driving function for 2.5-dimensional WFS of a virtual point source. - .. versionadded:: 0.61 + .. versionadded:: 0.6.1 `point_25d()` was renamed to `point_25d_legacy()` (and a new function with the name `point_25d()` was introduced). See notes below for further details.