diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..54f10553 --- /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.11', '3.12'] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + 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 113bf3b1..00000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: python -dist: xenial -matrix: - include: - - python: "3.5" - - python: "3.6" - - python: "3.7" -addons: - apt: - packages: - - pandoc - - ffmpeg -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 and runs the doctests: - - python -m sphinx doc/ _build/ -b doctest diff --git a/NEWS.rst b/NEWS.rst index 6df799c6..0a1d06d2 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,6 +1,17 @@ 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 + +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/doc/conf.py b/doc/conf.py index a319efa1..00865f9b 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', @@ -46,6 +45,8 @@ 'nbsphinx', ] +bibtex_bibfiles = ['references.bib'] + nbsphinx_execute_arguments = [ "--InlineBackend.figure_formats={'svg', 'pdf'}", "--InlineBackend.rc={'figure.dpi': 96}", @@ -56,7 +57,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'] @@ -79,7 +83,7 @@ } extlinks = {'sfs': ('https://sfs.readthedocs.io/en/3.2/%s', - 'https://sfs.rtfd.io/')} + 'https://sfs.rtfd.io/%s')} plot_include_source = True plot_html_show_source_link = False @@ -90,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 }, @@ -214,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. diff --git a/doc/examples/animations-pulsating-sphere.ipynb b/doc/examples/animations-pulsating-sphere.ipynb index f8b1e22f..3ed8d990 100644 --- a/doc/examples/animations-pulsating-sphere.ipynb +++ b/doc/examples/animations-pulsating-sphere.ipynb @@ -147,7 +147,10 @@ "metadata": {}, "outputs": [], "source": [ - "grid = hex_grid([xmin, xmax], [ymin, ymax], 0.0125, 'vertical')\n", + "grid = sfs.util.as_xyz_components(hex_grid([xmin, xmax],\n", + " [ymin, ymax],\n", + " 0.0125,\n", + " 'vertical'))\n", "ani = animation.particle_displacement(\n", " omega, center, radius, amplitude, grid, frames, figsize, c='Gray')\n", "plt.close()\n", @@ -167,8 +170,9 @@ "metadata": {}, "outputs": [], "source": [ - "grid = [np.random.uniform(xmin, xmax, 4000),\n", - " np.random.uniform(ymin, ymax, 4000), 0]\n", + "grid = sfs.util.as_xyz_components([np.random.uniform(xmin, xmax, 4000),\n", + " np.random.uniform(ymin, ymax, 4000),\n", + " 0])\n", "ani = animation.particle_displacement(\n", " omega, center, radius, amplitude, grid, frames, figsize, c='Gray')\n", "plt.close()\n", diff --git a/doc/examples/animations_pulsating_sphere.py b/doc/examples/animations_pulsating_sphere.py index a4150679..a5fae46c 100644 --- a/doc/examples/animations_pulsating_sphere.py +++ b/doc/examples/animations_pulsating_sphere.py @@ -3,6 +3,8 @@ import numpy as np from matplotlib import pyplot as plt from matplotlib import animation +import warnings +warnings.simplefilter("ignore", np.exceptions.ComplexWarning) def particle_displacement(omega, center, radius, amplitude, grid, frames, @@ -18,7 +20,7 @@ def particle_displacement(omega, center, radius, amplitude, grid, frames, scat = sfs.plot2d.particles(grid + displacement, **kwargs) def update_frame_displacement(i): - position = (grid + displacement * phasor**i).apply(np.real) + position = np.real((grid + displacement * phasor**i)) position = np.column_stack([position[0].flatten(), position[1].flatten()]) scat.set_offsets(position) @@ -43,7 +45,7 @@ def particle_velocity(omega, center, radius, amplitude, grid, frames, **kwargs) def update_frame_velocity(i): - quiv.set_UVC(*(velocity[:2] * phasor**i).apply(np.real)) + np.real(quiv.set_UVC(*(velocity[:2] * phasor**i))) return [quiv] return animation.FuncAnimation( diff --git a/doc/examples/plot_particle_density.py b/doc/examples/plot_particle_density.py index b21cd20c..85471199 100644 --- a/doc/examples/plot_particle_density.py +++ b/doc/examples/plot_particle_density.py @@ -14,7 +14,9 @@ # normal vector of plane wave npw = sfs.util.direction_vector(np.radians(pw_angle)) # random grid for velocity -grid = [np.random.uniform(-3, 3, 40000), np.random.uniform(-3, 3, 40000), 0] +grid = sfs.util.as_xyz_components([np.random.uniform(-3, 3, 40000), + np.random.uniform(-3, 3, 40000), + 0]) def plot_particle_displacement(title): diff --git a/doc/examples/time_domain_nfchoa.py b/doc/examples/time_domain_nfchoa.py index 929c1ef8..19563a00 100644 --- a/doc/examples/time_domain_nfchoa.py +++ b/doc/examples/time_domain_nfchoa.py @@ -33,7 +33,7 @@ plt.savefig('impulse_pw_nfchoa_25d.png') # Point source -max_order = 100 +max_order = 80 xs = [1.5, 1.5, 0] # position t = np.linalg.norm(xs) / sfs.default.c # observation time delay, weight, sos, phaseshift, selection, secondary_source = \ diff --git a/doc/readthedocs-environment.yml b/doc/readthedocs-environment.yml deleted file mode 100644 index d5ff0570..00000000 --- a/doc/readthedocs-environment.yml +++ /dev/null @@ -1,14 +0,0 @@ -channels: - - conda-forge -dependencies: - - python>=3 - - sphinx>=1.3.6 - - sphinx_rtd_theme - - sphinxcontrib-bibtex - - numpy - - scipy - - matplotlib>=1.5 - - ipykernel - - pandoc - - pip: - - nbsphinx diff --git a/doc/references.rst b/doc/references.rst index 4b3db95d..8905446f 100644 --- a/doc/references.rst +++ b/doc/references.rst @@ -1,6 +1,5 @@ References ========== -.. bibliography:: references.bib +.. bibliography:: :style: alpha - :all: diff --git a/doc/requirements.txt b/doc/requirements.txt index e2e3ce91..4f038ca3 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 diff --git a/readthedocs.yml b/readthedocs.yml index 65bb0f41..7ff6f6a9 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -1,4 +1,11 @@ -conda: - file: doc/readthedocs-environment.yml +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.8" + python: - pip_install: true + install: + - requirements: requirements.txt + - requirements: doc/requirements.txt diff --git a/setup.py b/setup.py index fc2bee52..e0f01888 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ version=__version__, packages=find_packages(), install_requires=[ - 'numpy!=1.11.0', # https://github.com/sfstoolbox/sfs-python/issues/11 + 'numpy', 'scipy', ], author="SFS Toolbox Developers", @@ -24,16 +24,16 @@ 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.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3 :: Only", "Topic :: Scientific/Engineering", ], diff --git a/sfs/__init__.py b/sfs/__init__.py index 2072d811..6e3d57c5 100644 --- a/sfs/__init__.py +++ b/sfs/__init__.py @@ -16,7 +16,7 @@ util """ -__version__ = "0.5.0" +__version__ = "0.6.2" class default: diff --git a/sfs/array.py b/sfs/array.py index 899075cb..148a5b5f 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)) @@ -656,7 +664,7 @@ def weights_midpoint(positions, *, closed): >>> x0, n0, a0 = sfs.array.circular(2**5, 1) >>> a = sfs.array.weights_midpoint(x0, closed=True) >>> max(abs(a0-a)) - 0.0003152601902411123 + np.float64(0.0003152601902411123) """ positions = _util.asarray_of_rows(positions) @@ -664,7 +672,7 @@ def weights_midpoint(positions, *, closed): before, after = -1, 0 # cyclic else: before, after = 1, -2 # mirrored - positions = _np.row_stack((positions[before], positions, positions[after])) + positions = _np.vstack((positions[before], positions, positions[after])) distances = _np.linalg.norm(_np.diff(positions, axis=0), axis=1) return (distances[:-1] + distances[1:]) / 2 diff --git a/sfs/fd/__init__.py b/sfs/fd/__init__.py index 2d9555e4..1dfe2cb6 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/fd/wfs.py b/sfs/fd/wfs.py index 817b4eb8..840b59e9 100644 --- a/sfs/fd/wfs.py +++ b/sfs/fd/wfs.py @@ -32,7 +32,6 @@ def plot(d, selection, secondary_source): """ import numpy as _np -from numpy.core.umath_tests import inner1d as _inner1d from scipy.special import hankel2 as _hankel2 from . import secondary_source_line as _secondary_source_line @@ -91,7 +90,7 @@ def line_2d(omega, x0, n0, xs, *, c=None): 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) + d = -1j/2 * k * _util._inner1d(ds, n0) / r * _hankel2(1, k * r) selection = _util.source_selection_line(n0, x0, xs) return d, selection, _secondary_source_line(omega, c) @@ -147,7 +146,7 @@ def _point(omega, x0, n0, xs, *, c=None): 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) + d = 1j * k * _util._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) @@ -171,9 +170,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 @@ -223,7 +221,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 @@ -235,7 +233,7 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): preeq_25d(omega, omalias, c) * _np.sqrt(8 * _np.pi) * _np.sqrt((r * s) / (r + s)) * - _inner1d(n0, ds) / s * + _util._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) @@ -317,7 +315,7 @@ def point_25d_legacy(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): r = _np.linalg.norm(ds, axis=1) d = ( preeq_25d(omega, omalias, c) * - _np.sqrt(_np.linalg.norm(xref - x0)) * _inner1d(ds, n0) / + _np.sqrt(_np.linalg.norm(xref - x0)) * _util._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) @@ -500,7 +498,7 @@ def _focused(omega, x0, n0, xs, ns, *, c=None): 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) + d = 1j * k * _util._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) @@ -570,7 +568,7 @@ def focused_25d(omega, x0, n0, xs, ns, *, xref=[0, 0, 0], c=None, r = _np.linalg.norm(ds, axis=1) d = ( preeq_25d(omega, omalias, c) * - _np.sqrt(_np.linalg.norm(xref - x0)) * _inner1d(ds, n0) / + _np.sqrt(_np.linalg.norm(xref - x0)) * _util._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) diff --git a/sfs/plot2d.py b/sfs/plot2d.py index f05936e5..63f69966 100644 --- a/sfs/plot2d.py +++ b/sfs/plot2d.py @@ -18,13 +18,17 @@ def _register_cmap_clip(name, original_cmap, alpha): 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.colormaps.register(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/ -_register_cmap_clip('coolwarm_clip', 'coolwarm', 0.7) +# already registered in MPL 3.9.0 +try: + _register_cmap_clip('coolwarm_clip', 'coolwarm', 0.7) +except ImportError: + pass def _register_cmap_transparent(name, color): @@ -36,7 +40,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.colormaps.register(cmap=cmap) _register_cmap_transparent('blacktransparent', 'black') @@ -72,8 +76,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 +100,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) @@ -273,8 +289,8 @@ def amplitude(p, grid, *, xnorm=None, cmap='coolwarm_clip', 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] + dx = 0.5 * _np.ptp(x) / p.shape[0] + dy = 0.5 * _np.ptp(y) / p.shape[1] if ax is None: ax = _plt.gca() @@ -318,8 +334,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: @@ -336,7 +382,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) diff --git a/sfs/td/__init__.py b/sfs/td/__init__.py index 5efb20a5..a786ea54 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*. diff --git a/sfs/td/wfs.py b/sfs/td/wfs.py index ab2531f6..3e112082 100644 --- a/sfs/td/wfs.py +++ b/sfs/td/wfs.py @@ -39,11 +39,11 @@ 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 -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 @@ -92,9 +92,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. @@ -118,14 +118,107 @@ def plane_25d(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): 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) + delays = _util._inner1d(n, x0) / c + weights = 2 * g0 * _util._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. + r"""Driving function for 2.5-dimensional WFS of a virtual point source. + + .. versionchanged:: 0.6.1 + 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 + Contour xref(x0) for amplitude correct synthesis, reference point xref. + 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 * _util._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.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. Parameters ---------- @@ -166,15 +259,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:: @@ -196,7 +295,7 @@ def point_25d(x0, n0, xs, xref=[0, 0, 0], c=None): ds = x0 - xs r = _np.linalg.norm(ds, axis=1) delays = r/c - weights = g0 * _inner1d(ds, n0) / (2 * _np.pi * r**(3/2)) + weights = g0 * _util._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) @@ -248,10 +347,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. @@ -279,7 +378,7 @@ def focused_25d(x0, n0, xs, ns, xref=[0, 0, 0], c=None): 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)) + weights = g0 * _util._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) diff --git a/sfs/util.py b/sfs/util.py index b054346a..de2ead57 100644 --- a/sfs/util.py +++ b/sfs/util.py @@ -6,7 +6,6 @@ import collections import numpy as np -from numpy.core.umath_tests import inner1d from scipy.special import spherical_jn, spherical_yn from . import default @@ -576,7 +575,7 @@ def source_selection_point(n0, x0, xs): x0 = asarray_of_rows(x0) xs = asarray_1d(xs) ds = x0 - xs - return inner1d(ds, n0) >= default.selection_tolerance + return _inner1d(ds, n0) >= default.selection_tolerance def source_selection_line(n0, x0, xs): @@ -598,7 +597,7 @@ def source_selection_focused(ns, x0, xs): xs = asarray_1d(xs) ns = normalize_vector(ns) ds = xs - x0 - return inner1d(ns, ds) >= default.selection_tolerance + return _inner1d(ns, ds) >= default.selection_tolerance def source_selection_all(N): @@ -610,7 +609,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} = @@ -646,3 +645,8 @@ def max_order_spherical_harmonics(N): """ return int(np.sqrt(N) - 1) + + +def _inner1d(arr1, arr2): + # https://github.com/numpy/numpy/issues/10815#issuecomment-376847774 + return (arr1 * arr2).sum(axis=1)