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 61bb858..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-language: python
-python:
- - "2.7"
- - "3.6"
-addons:
- apt:
- packages:
- - pandoc
-install:
- - pip install .
- - pip install -r tests/requirements.txt
- - pip install -r doc/requirements.txt
-script:
- - python -m pytest
- # This executes the example notebooks and checks for valid URLs:
- - python -m sphinx doc/ _build/ -b linkcheck
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 68887d1..006d7e4 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -6,37 +6,79 @@ improvement, please create an issue or a pull request at
https://github.com/sfstoolbox/sfs-python/.
Contributions are always welcome!
-Instead of pip-installing the latest release from PyPI, you should get the
+Development Installation
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+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
- python 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::
+.. _PyPI: https://pypi.org/project/sfs/
+.. _Github: https://github.com/sfstoolbox/sfs-python/
- pip install --user -e .
-... where ``-e`` stands for ``--editable``.
+Building the Documentation
+^^^^^^^^^^^^^^^^^^^^^^^^^^
If you make changes to the documentation, you can re-create the HTML pages
using Sphinx_.
You can install it and a few other necessary packages with::
- pip install -r doc/requirements.txt --user
+ python3 -m pip install -r doc/requirements.txt --user
To create the HTML pages, use::
- python setup.py build_sphinx
+ python3 setup.py build_sphinx
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
+^^^^^^^^^^^^^^^^^
+
+You'll need pytest_ for that.
+It can be installed with::
+
+ python3 -m pip install -r tests/requirements.txt --user
+
+To execute the tests, simply run::
+
+ python3 -m pytest
+
+.. _pytest: https://pytest.org/
+
+Creating a New Release
+^^^^^^^^^^^^^^^^^^^^^^
New releases are made using the following steps:
@@ -46,14 +88,14 @@ New releases are made using the following steps:
#. Create an (annotated) tag with ``git tag -a x.y.z``
#. Clear the ``dist/`` directory
#. Create a source distribution with ``python3 setup.py sdist``
-#. Create a wheel distribution with ``python3 setup.py bdist_wheel --universal``
+#. 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.python.org/pypi/twine
+.. _twine: https://twine.readthedocs.io/
.. _add release notes: https://github.com/sfstoolbox/sfs-python/tags
-.. _RTD: http://readthedocs.org/projects/sfs/builds/
+.. _RTD: https://readthedocs.org/projects/sfs-python/builds/
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/NEWS.rst b/NEWS.rst
index 1a3eed8..0a1d06d 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,6 +1,49 @@
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()`
+ * 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
+ * Image source model for a point source in a rectangular room
+ * `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 `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 `sfs.util.cart2sph()`
+ * Fix `sfs.tapering.tukey()` for ``alpha=1``
+
Version 0.3.1 (2016-04-08):
* Fixed metadata of release
@@ -9,12 +52,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
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/.
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/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/_static/css/title.css b/doc/_static/css/title.css
new file mode 100644
index 0000000..e539e2b
--- /dev/null
+++ b/doc/_static/css/title.css
@@ -0,0 +1,33 @@
+.wy-side-nav-search>a, .wy-side-nav-search .wy-dropdown>a {
+ font-family: "Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif;
+ font-size: 200%;
+ margin-top: .222em;
+ margin-bottom: .202em;
+}
+.wy-side-nav-search {
+ padding: 0;
+}
+form#rtd-search-form {
+ margin-left: .809em;
+ margin-right: .809em;
+}
+.rtd-nav a {
+ float: left;
+ display: block;
+ width: 33.3%;
+ height: 100%;
+ padding-top: 7px;
+ color: white;
+}
+.rtd-nav {
+ overflow: hidden;
+ width: 100%;
+ height: 35px;
+ margin-top: 15px;
+}
+.rtd-nav a:hover {
+ background-color: #388bbd;
+}
+.rtd-nav a.active {
+ background-color: #388bbd;
+}
diff --git a/doc/_template/layout.html b/doc/_template/layout.html
new file mode 100644
index 0000000..36a3c25
--- /dev/null
+++ b/doc/_template/layout.html
@@ -0,0 +1,14 @@
+{% extends "!layout.html" %}
+{% block sidebartitle %}
+
+ {{ project }}
+
+ {% include "searchbox.html" %}
+
+
+
+{% endblock %}
diff --git a/doc/api.rst b/doc/api.rst
new file mode 100644
index 0000000..1e66983
--- /dev/null
+++ b/doc/api.rst
@@ -0,0 +1,4 @@
+API Documentation
+=================
+
+.. automodule:: sfs
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 0a67eae..35b2874 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.
@@ -32,24 +34,35 @@
# ones.
extensions = [
'sphinx.ext.autodoc',
- 'sphinx.ext.mathjax',
+ 'sphinx.ext.autosummary',
'sphinx.ext.viewcode',
'sphinx.ext.napoleon', # support for NumPy-style docstrings
'sphinx.ext.intersphinx',
- 'sphinx.ext.extlinks',
+ 'sphinx.ext.doctest',
'sphinxcontrib.bibtex',
+ 'sphinx.ext.extlinks',
'matplotlib.sphinxext.plot_directive',
'nbsphinx',
]
-# Override kernel name to allow running with Python 2 on Travis-CI
-nbsphinx_kernel_name = 'python'
+bibtex_bibfiles = ['references.bib']
+
+nbsphinx_execute_arguments = [
+ "--InlineBackend.figure_formats={'svg', 'pdf'}",
+ "--InlineBackend.rc={'figure.dpi': 96}",
+]
-linkcheck_timeout = 10
+# 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']
+autodoc_default_options = {
+ 'members': True,
+ 'undoc-members': True,
+}
+
+autosummary_generate = ['api']
napoleon_google_docstring = False
napoleon_numpy_docstring = True
@@ -66,16 +79,44 @@
'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),
}
+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
-plot_pre_code = ""
+plot_pre_code = ''
+plot_rcparams = {
+ 'savefig.bbox': 'tight',
+}
+plot_formats = ['svg', 'pdf']
+
+# 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
+ },
+}
# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ['_template']
# The suffix of source filenames.
source_suffix = '.rst'
@@ -88,8 +129,8 @@
# General information about the project.
authors = 'SFS Toolbox Developers'
-project = 'Sound Field Synthesis Toolbox'
-copyright = '2017, ' + authors
+project = 'SFS Toolbox'
+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
@@ -104,10 +145,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
@@ -117,6 +154,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.
@@ -146,9 +188,53 @@
# 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:`
`
+
+ __ 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 ----------------------------------------------
+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.
html_theme = 'sphinx_rtd_theme'
@@ -215,6 +301,7 @@
# 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
@@ -233,20 +320,32 @@
# 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
diff --git a/doc/example-python-scripts.rst b/doc/example-python-scripts.rst
new file mode 100644
index 0000000..2e38f11
--- /dev/null
+++ b/doc/example-python-scripts.rst
@@ -0,0 +1,12 @@
+Example Python Scripts
+======================
+
+Various example scripts are located in the directory ``doc/examples/``, e.g.
+
+* :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
diff --git a/doc/examples.rst b/doc/examples.rst
new file mode 100644
index 0000000..0248604
--- /dev/null
+++ b/doc/examples.rst
@@ -0,0 +1,20 @@
+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/sound-field-synthesis
+ 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..f8b1e22
--- /dev/null
+++ b/doc/examples/animations-pulsating-sphere.ipynb
@@ -0,0 +1,359 @@
+{
+ "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.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",
+ "- [sfs.fd.displacement()](../sfs.fd.rst#sfs.fd.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_jshtml())"
+ ]
+ },
+ {
+ "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/)."
+ ]
+ },
+ {
+ "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_jshtml())"
+ ]
+ },
+ {
+ "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_jshtml())"
+ ]
+ },
+ {
+ "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_jshtml())"
+ ]
+ },
+ {
+ "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_jshtml())"
+ ]
+ },
+ {
+ "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_jshtml())"
+ ]
+ },
+ {
+ "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_jshtml())"
+ ]
+ },
+ {
+ "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..a415067
--- /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.fd.source.pulsating_sphere_velocity(
+ omega, center, radius, amplitude, grid)
+ displacement = sfs.fd.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.plot2d.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.fd.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.plot2d.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.fd.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.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):
+ 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/doc/examples/horizontal_plane_arrays.py b/doc/examples/horizontal_plane_arrays.py
index cb42a36..daba525 100644
--- a/doc/examples/horizontal_plane_arrays.py
+++ b/doc/examples/horizontal_plane_arrays.py
@@ -29,126 +29,123 @@ 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, alpha=talpha)
+ p = sfs.fd.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.virtualsource_2d(xs)
- sfs.plot.virtualsource_2d([0, 0], npw, type='plane')
+ 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')
plt.title(title)
plt.grid()
plt.savefig(title + '.png')
# 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.mono.drivingfunction.source_selection_point(n0, x0, xs)
-
-d = sfs.mono.drivingfunction.wfs_3d_point(omega, x0, n0, xs)
+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 = sfs.mono.drivingfunction.wfs_25d_point(omega, x0, n0, xs, xref=xnorm)
+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 = sfs.mono.drivingfunction.wfs_2d_point(omega, x0, n0, xs)
+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
-sourcetype = sfs.mono.source.line
-d = sfs.mono.drivingfunction.wfs_2d_line(omega, x0, n0, xs)
+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
-sourcetype = sfs.mono.source.point
-a = sfs.mono.drivingfunction.source_selection_plane(n0, npw)
-
-d = sfs.mono.drivingfunction.wfs_3d_plane(omega, x0, n0, npw)
+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 = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref=xnorm)
+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 = sfs.mono.drivingfunction.wfs_2d_plane(omega, x0, n0, npw)
+d, selection, secondary_source = sfs.fd.wfs.plane_2d(
+ 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.mono.drivingfunction.source_selection_point(n0, x0, xs)
+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 = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref=xnorm)
-a = sfs.mono.drivingfunction.source_selection_plane(n0, npw)
+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')
# 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.mono.drivingfunction.source_selection_point(n0, x0, xs)
+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 = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref=xnorm)
-a = sfs.mono.drivingfunction.source_selection_plane(n0, npw)
+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
-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)
+array = sfs.array.rectangular((N, N//2), dx, center=acenter, orientation=anormal)
+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 = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref=xnorm)
-a = sfs.mono.drivingfunction.source_selection_plane(n0, npw)
+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')
# 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.mono.drivingfunction.source_selection_point(n0, x0, xs)
+array = sfs.array.circular(N, 1, center=acenter)
+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 = sfs.mono.drivingfunction.wfs_25d_plane(omega, x0, n0, npw, xref=xnorm)
-a = sfs.mono.drivingfunction.source_selection_plane(n0, npw)
+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')
# 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.mono.drivingfunction.source_selection_all(N)
+d, selection, secondary_source = sfs.fd.nfchoa.plane_2d(
+ 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.mono.drivingfunction.source_selection_all(N)
+d, selection, secondary_source = sfs.fd.nfchoa.point_25d(
+ 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.mono.drivingfunction.source_selection_all(N)
+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/index.rst b/doc/examples/index.rst
deleted file mode 100644
index b2f707d..0000000
--- a/doc/examples/index.rst
+++ /dev/null
@@ -1,22 +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
diff --git a/doc/examples/ipython_kernel_config.py b/doc/examples/ipython_kernel_config.py
new file mode 100644
index 0000000..4cbbcc1
--- /dev/null
+++ b/doc/examples/ipython_kernel_config.py
@@ -0,0 +1,6 @@
+# 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'}
+c.InlineBackend.rc = {'figure.dpi': 96}
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
diff --git a/doc/examples/mirror-image-source-model.ipynb b/doc/examples/mirror-image-source-model.ipynb
new file mode 100644
index 0000000..c436dd2
--- /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.fd.source.point_image_sources(omega, x0, grid, L,\n",
+ " max_order=max_order, coeffs=coeffs)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "sfs.plot2d.amplitude(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.td.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.plot2d.level(p, grid)\n",
+ "sfs.plot2d.virtualsource(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')
diff --git a/doc/examples/modal-room-acoustics.ipynb b/doc/examples/modal-room-acoustics.ipynb
index 1468b37..26164bd 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.fd.source.point_modal(omega, x0, grid, L, N=N, deltan=deltan)"
]
},
{
@@ -115,7 +114,7 @@
"metadata": {},
"outputs": [],
"source": [
- "sfs.plot.soundfield(p, grid);"
+ "sfs.plot2d.amplitude(p, grid);"
]
},
{
@@ -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.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 1322fe0..b21cd20 100644
--- a/doc/examples/plot_particle_density.py
+++ b/doc/examples/plot_particle_density.py
@@ -19,11 +19,11 @@
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()
- 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()
@@ -31,16 +31,16 @@ def plot_particle_displacement(title):
# point source
-v = sfs.mono.source.point_velocity(omega, xs, npw, 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, npw, 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/run_all.py b/doc/examples/run_all.py
new file mode 100755
index 0000000..c446af7
--- /dev/null
+++ b/doc/examples/run_all.py
@@ -0,0 +1,24 @@
+#!/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
+ 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))
+ if result.returncode:
+ print('Error running', script, file=sys.stderr)
+ sys.exit(result.returncode)
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 a664889..0000000
--- a/doc/examples/sound_field_synthesis.py
+++ /dev/null
@@ -1,87 +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), np.radians(90))
-
-
-# === 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')
-
-#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))
-
-#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])
-
-
-# === compute driving function ===
-#d = sfs.mono.drivingfunction.delay_3d_plane(omega, x0, n0, npw)
-
-#d = sfs.mono.drivingfunction.wfs_2d_line(omega, x0, n0, 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 = 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 = sfs.mono.drivingfunction.nfchoa_2d_plane(omega, x0, 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.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))
-
-
-# === compute tapering window ===
-#twin = sfs.tapering.none(a)
-#twin = sfs.tapering.kaiser(a, 8.6)
-twin = sfs.tapering.tukey(a,.3)
-
-# === compute synthesized sound field ===
-p = sfs.mono.synthesized.generic(omega, x0, n0, d * twin * a0 , grid,
- source=sfs.mono.source.point)
-
-
-# === plot synthesized sound field ===
-plt.figure(figsize=(10, 10))
-sfs.plot.soundfield(p, grid, [0, 0, 0])
-sfs.plot.loudspeaker_2d(x0, n0, twin)
-plt.grid()
-plt.savefig('soundfield.png')
-
-
-#sfs.plot.loudspeaker_3d(x0, n0, twin)
-#plt.savefig('loudspeakers.png')
diff --git a/doc/examples/soundfigures.py b/doc/examples/soundfigures.py
index e337789..6bb763b 100644
--- a/doc/examples/soundfigures.py
+++ b/doc/examples/soundfigures.py
@@ -24,27 +24,27 @@
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.fd.wfs.soundfigure_3d(
+ 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.fd.synthesize(d, selection, array, secondary_source, grid=grid)
# 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 06c3374..9ad14a4 100644
--- a/doc/examples/time_domain.py
+++ b/doc/examples/time_domain.py
@@ -14,30 +14,32 @@
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 = sfs.time.drivingfunction.driving_signals(d_delay, d_weight, signal)
+d_delay, d_weight, selection, secondary_source = \
+ sfs.td.wfs.point_25d(array.x, array.n, xs)
+d = sfs.td.wfs.driving_signals(d_delay, d_weight, signal)
# test soundfield
-a = sfs.mono.drivingfunction.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, alpha=0.3)
+
+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))
-sfs.plot.level(p, grid, cmap=my_cmap)
-sfs.plot.loudspeaker_2d(x0, n0, 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')
@@ -47,19 +49,20 @@
t = -0.001
# compute driving signals
-d_delay, d_weight = sfs.time.drivingfunction.wfs_25d_plane(x0, n0, npw)
-d = sfs.time.drivingfunction.driving_signals(d_delay, d_weight, signal)
+d_delay, d_weight, selection, secondary_source = \
+ sfs.td.wfs.plane_25d(array.x, array.n, npw)
+d = sfs.td.wfs.driving_signals(d_delay, d_weight, signal)
# test soundfield
-a = sfs.mono.drivingfunction.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, alpha=0.3)
+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)
-sfs.plot.loudspeaker_2d(x0, n0, 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')
@@ -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 = sfs.time.drivingfunction.driving_signals(d_delay, d_weight, signal)
+d_delay, d_weight, selection, secondary_source = \
+ sfs.td.wfs.focused_25d(array.x, array.n, xs, nfs)
+d = sfs.td.wfs.driving_signals(d_delay, d_weight, signal)
# test soundfield
-a = sfs.mono.drivingfunction.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, alpha=0.3)
+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))
-sfs.plot.level(p, grid, cmap=my_cmap)
-sfs.plot.loudspeaker_2d(x0, n0, 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')
-
-# plt.show()
diff --git a/doc/examples/time_domain_nfchoa.py b/doc/examples/time_domain_nfchoa.py
new file mode 100644
index 0000000..929c1ef
--- /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.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.td.synthesize(d, selection, array, secondary_source,
+ observation_time=t, grid=grid)
+
+plt.figure()
+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
+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.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.td.synthesize(d, selection, array, secondary_source,
+ observation_time=t, grid=grid)
+
+plt.figure()
+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/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..e56a16f 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -5,12 +5,8 @@
.. toctree::
installation
- examples/index
- arrays
- frequency-domain
- time-domain
- plotting
- utilities
+ examples
+ api
references
contributing
version-history
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/
diff --git a/doc/math-definitions.rst b/doc/math-definitions.rst
index ee88d28..cec4c17 100644
--- a/doc/math-definitions.rst
+++ b/doc/math-definitions.rst
@@ -1,10 +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}}
+
+ \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)}
+ \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
+
+ }
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/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/doc/references.bib b/doc/references.bib
index d1c2c02..8b149ba 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},
@@ -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/doc/references.rst b/doc/references.rst
index 4b3db95..8905446 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 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
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/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
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
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..0c9e0fc
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[metadata]
+license_file = LICENSE
diff --git a/setup.py b/setup.py
index 79cd4a5..9a7d870 100644
--- a/setup.py
+++ b/setup.py
@@ -24,11 +24,16 @@
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",
"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,
diff --git a/sfs/__init__.py b/sfs/__init__.py
index da5048e..6e3d57c 100644
--- a/sfs/__init__.py
+++ b/sfs/__init__.py
@@ -1,18 +1,72 @@
"""Sound Field Synthesis Toolbox.
-http://sfs.rtfd.org/
+https://sfs-python.readthedocs.io/
+
+.. rubric:: Submodules
+
+.. autosummary::
+ :toctree:
+
+ fd
+ td
+ array
+ tapering
+ plot2d
+ plot3d
+ util
"""
-__version__ = "0.3.1"
+__version__ = "0.6.2"
+
+
+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
+ from . import plot2d
+except ImportError:
+ pass
+try:
+ from . import plot3d
except ImportError:
pass
-from . import mono
-from . import time
+from . import fd
+from . import td
diff --git a/sfs/array.py b/sfs/array.py
index 9a787ec..1727475 100644
--- a/sfs/array.py
+++ b/sfs/array.py
@@ -8,17 +8,16 @@
plt.rcParams['figure.figsize'] = 8, 4.5 # inch
plt.rcParams['axes.grid'] = True
-.. autoclass:: ArrayData
- :members: take
-
"""
-from __future__ import division # for Python 2.x
-from collections import namedtuple
-import numpy as np
-from . import util
+from collections import namedtuple as _namedtuple
+
+import numpy as _np
+
+from . import util as _util
-class ArrayData(namedtuple('ArrayData', 'x n a')):
+class SecondarySourceDistribution(_namedtuple('SecondarySourceDistribution',
+ 'x n a')):
"""Named tuple returned by array functions.
See `collections.namedtuple`.
@@ -37,17 +36,58 @@ class ArrayData(namedtuple('ArrayData', 'x n a')):
__slots__ = ()
def __repr__(self):
- return 'ArrayData(\n' + ',\n'.join(
- ' {0}={1}'.format(name, repr(data).replace('\n', '\n '))
+ 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 linear(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]):
- """Linear secondary source distribution.
+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]):
+ """Return linear, equidistantly sampled secondary source distribution.
Parameters
----------
@@ -63,7 +103,7 @@ def linear(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]):
Returns
-------
- `ArrayData`
+ `SecondarySourceDistribution`
Positions, orientations and weights of secondary sources.
Examples
@@ -72,15 +112,17 @@ 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')
"""
- 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]):
- """Linear secondary source distribution from a list of distances.
+def linear_diff(distances, *, center=[0, 0, 0], orientation=[1, 0, 0]):
+ """Return linear secondary source distribution from a list of distances.
Parameters
----------
@@ -91,7 +133,7 @@ def linear_diff(distances, center=[0, 0, 0], orientation=[1, 0, 0]):
Returns
-------
- `ArrayData`
+ `SecondarySourceDistribution`
Positions, orientations and weights of secondary sources.
Examples
@@ -101,18 +143,20 @@ 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')
"""
- 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)
-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):
- """Randomly sampled linear array.
+ """Return randomly sampled linear array.
Parameters
----------
@@ -128,7 +172,7 @@ def linear_random(N, min_spacing, max_spacing, center=[0, 0, 0],
Returns
-------
- `ArrayData`
+ `SecondarySourceDistribution`
Positions, orientations and weights of secondary sources.
Examples
@@ -136,18 +180,23 @@ 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])
- sfs.plot.loudspeaker_2d(x0, n0, a0)
+ x0, n0, a0 = sfs.array.linear_random(
+ N=12,
+ min_spacing=0.15, max_spacing=0.4,
+ orientation=[0, -1, 0])
+ sfs.plot2d.loudspeakers(x0, n0, a0)
plt.axis('equal')
+ plt.xlabel('x / m')
+ 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, orientation)
+ return linear_diff(distances, center=center, orientation=orientation)
-def circular(N, R, center=[0, 0, 0]):
- """Circular secondary source distribution parallel to the xy-plane.
+def circular(N, R, *, center=[0, 0, 0]):
+ """Return circular secondary source distribution parallel to the xy-plane.
Parameters
----------
@@ -160,7 +209,7 @@ def circular(N, R, center=[0, 0, 0]):
Returns
-------
- `ArrayData`
+ `SecondarySourceDistribution`
Positions, orientations and weights of secondary sources.
Examples
@@ -169,25 +218,27 @@ 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')
"""
- 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
- return ArrayData(positions, normals, weights)
+ 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)
-def rectangular(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]):
- """Rectangular secondary source distribution.
+def rectangular(N, spacing, *, center=[0, 0, 0], orientation=[1, 0, 0]):
+ """Return rectangular secondary source distribution.
Parameters
----------
@@ -204,7 +255,7 @@ def rectangular(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]):
Returns
-------
- `ArrayData`
+ `SecondarySourceDistribution`
Positions, orientations and weights of secondary sources.
Examples
@@ -213,27 +264,33 @@ 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')
"""
- 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(
- 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)
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]):
- """Array along the xy-axis with rounded edge at the origin.
+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
----------
@@ -242,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
@@ -249,7 +308,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
@@ -258,59 +317,65 @@ 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')
"""
# 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 = np.flipud(x00)
+ x00, n00, a00 = linear(Nxy, spacing,
+ center=[0, Nxy//2*spacing+spacing/2+R, 0])
+ 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)
- a00[n] = dx
- positions = np.concatenate((positions, x00))
- directions = np.concatenate((directions, n00))
- weights = np.concatenate((weights, a00))
+ 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] = 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))
- 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,
[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]):
- """Array along the xy-axis with edge at the origin.
+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
@@ -318,7 +383,7 @@ def edge(Nxy, dx, center=[0, 0, 0], orientation=[1, 0, 0]):
Returns
-------
- `ArrayData`
+ `SecondarySourceDistribution`
Positions, orientations and weights of secondary sources.
Examples
@@ -327,35 +392,39 @@ 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')
"""
# array along y-axis
- x00, n00, a00 = linear(Nxy, dx, center=[0, Nxy//2*dx+dx/2, 0])
- x00 = np.flipud(x00)
+ 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))
- 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,
[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]):
- """Planar secondary source distribtion.
+def planar(N, spacing, *, center=[0, 0, 0], orientation=[1, 0, 0]):
+ """Return planar secondary source distribtion.
Parameters
----------
@@ -371,24 +440,42 @@ def planar(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]):
Returns
-------
- `ArrayData`
+ `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.plot2d.loudspeakers(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
- 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
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]):
- """Cube-shaped secondary source distribtion.
+def cube(N, spacing, *, center=[0, 0, 0], orientation=[1, 0, 0]):
+ """Return cube-shaped secondary source distribtion.
Parameters
----------
@@ -404,69 +491,152 @@ def cube(N, spacing, center=[0, 0, 0], orientation=[1, 0, 0]):
Returns
-------
- `ArrayData`
+ `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.plot2d.loudspeakers(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)
- offset2 = spacing * (N1 - 1) / 2 + spacing / np.sqrt(2)
- offset3 = spacing * (N3 - 1) / 2 + spacing / np.sqrt(2)
+ 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)
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)
positions += center
- return ArrayData(positions, directions, weights)
+ 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
-------
- `ArrayData`
+ `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.plot2d.loudspeakers(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
positions += center
- return ArrayData(positions, normals, weights)
+ 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
-------
- `ArrayData`
+ `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.plot2d.loudspeakers(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.plot2d.loudspeakers(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)
positions += center
- return ArrayData(positions, normals, weights)
+ 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.
@@ -477,8 +647,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.
@@ -488,39 +658,69 @@ 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)
+ 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 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.
+
+ 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.plot2d.loudspeakers(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))
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/fd/__init__.py b/sfs/fd/__init__.py
new file mode 100644
index 0000000..1dfe2cb
--- /dev/null
+++ b/sfs/fd/__init__.py
@@ -0,0 +1,95 @@
+"""Submodules for monochromatic sound fields.
+
+.. autosummary::
+ :toctree:
+
+ source
+
+ wfs
+ nfchoa
+ sdm
+ esa
+
+"""
+import numpy as _np
+
+from . import source
+from .. import array as _array
+from .. import util as _util
+
+
+def shiftphase(p, phase):
+ """Shift phase of a sound field."""
+ p = _np.asarray(p)
+ 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.
+
+ 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, **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
+
+
+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=c)
+
+ return secondary_source
+
+
+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=c)
+
+ return secondary_source
+
+
+from . import esa
+from . import nfchoa
+from . import sdm
+from . import wfs
diff --git a/sfs/fd/esa.py b/sfs/fd/esa.py
new file mode 100644
index 0000000..380769c
--- /dev/null
+++ b/sfs/fd/esa.py
@@ -0,0 +1,360 @@
+"""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 as _jn, hankel2 as _hankel2
+
+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=_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
+ 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.fd.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=_np.pi*3/2,
+ 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.fd.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=_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
+ 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.fd.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=_np.pi*3/2, 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.fd.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=_np.pi*3/2,
+ 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.fd.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/fd/nfchoa.py b/sfs/fd/nfchoa.py
new file mode 100644
index 0000000..d41c2b7
--- /dev/null
+++ b/sfs/fd/nfchoa.py
@@ -0,0 +1,237 @@
+"""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.fd.synthesize(d, selection, array, secondary_source, grid=grid)
+ sfs.plot2d.amplitude(p, grid)
+ sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15)
+
+"""
+import numpy as _np
+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
+
+
+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.fd.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 :sfs:`d_nfchoa/#equation-fd-nfchoa-plane-2d`
+
+ Examples
+ --------
+ .. plot::
+ :context: close-figs
+
+ d, selection, secondary_source = sfs.fd.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_line(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.fd.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 :sfs:`d_nfchoa/#equation-fd-nfchoa-point-25d`
+
+ Examples
+ --------
+ .. plot::
+ :context: close-figs
+
+ d, selection, secondary_source = sfs.fd.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.fd.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 :sfs:`d_nfchoa/#equation-fd-nfchoa-plane-25d`
+
+ Examples
+ --------
+ .. plot::
+ :context: close-figs
+
+ d, selection, secondary_source = sfs.fd.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/fd/sdm.py b/sfs/fd/sdm.py
new file mode 100644
index 0000000..7682e3d
--- /dev/null
+++ b/sfs/fd/sdm.py
@@ -0,0 +1,255 @@
+"""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.fd.synthesize(d, selection, array, secondary_source, grid=grid)
+ sfs.plot2d.amplitude(p, grid)
+ sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15)
+
+"""
+import numpy as _np
+from scipy.special import hankel2 as _hankel2
+
+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):
+ 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.fd.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.fd.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.fd.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.fd.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.fd.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.fd.sdm.plane_25d(
+ omega, array.x, array.n, npw, xref=[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.fd.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.fd.sdm.point_25d(
+ omega, array.x, array.n, xs, xref=[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/fd/source.py b/sfs/fd/source.py
new file mode 100644
index 0000000..6a0b860
--- /dev/null
+++ b/sfs/fd/source.py
@@ -0,0 +1,948 @@
+r"""Compute the sound field generated by a sound source.
+
+.. include:: math-definitions.rst
+
+.. plot::
+ :context: reset
+
+ import sfs
+ import numpy as np
+ import matplotlib.pyplot as plt
+ plt.rcParams['figure.figsize'] = 8, 4.5 # inch
+
+ x0 = 1.5, 1, 0
+ f = 500 # Hz
+ omega = 2 * np.pi * f
+
+ normalization_point = 4 * np.pi
+ 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)
+
+ # Grid for vector fields:
+ vgrid = sfs.util.xyz_grid([-2, 3], [-1, 2], 0, spacing=0.1)
+
+"""
+from itertools import product as _product
+
+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):
+ r"""Sound pressure of a point source.
+
+ Parameters
+ ----------
+ omega : float
+ Frequency of source.
+ x0 : (3,) array_like
+ Position of 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.
+
+ Returns
+ -------
+ numpy.ndarray
+ Sound pressure at positions given by *grid*.
+
+ Notes
+ -----
+ .. math::
+
+ G(\x-\x_0,\w) = \frac{1}{4\pi} \frac{\e{-\i\wc|\x-\x_0|}}{|\x-\x_0|}
+
+ Examples
+ --------
+ .. plot::
+ :context: close-figs
+
+ p = sfs.fd.source.point(omega, x0, grid)
+ sfs.plot2d.amplitude(p, grid)
+ plt.title("Point Source at {} m".format(x0))
+
+ Normalization ...
+
+ .. plot::
+ :context: close-figs
+
+ sfs.plot2d.amplitude(p * normalization_point, grid,
+ colorbar_kwargs=dict(label="p / Pa"))
+ 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)
+
+ 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'):
+ return numerator / r
+
+
+def point_velocity(omega, x0, grid, *, c=None, rho0=None):
+ """Particle velocity of a point source.
+
+ Parameters
+ ----------
+ omega : float
+ Frequency of source.
+ x0 : (3,) array_like
+ Position of 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.
+ rho0 : float, optional
+ Static density of air.
+
+ Returns
+ -------
+ `XyzComponents`
+ Particle velocity at positions given by *grid*.
+
+ Examples
+ --------
+ The particle velocity can be plotted on top of the sound pressure:
+
+ .. plot::
+ :context: close-figs
+
+ v = sfs.fd.source.point_velocity(omega, x0, vgrid)
+ sfs.plot2d.amplitude(p * normalization_point, grid)
+ sfs.plot2d.vectors(v * normalization_point, vgrid)
+ plt.title("Sound Pressure and Particle Velocity")
+
+ """
+ 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, 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, grid, *, c=None, rho0=None):
+ """Velocity of a point source.
+
+ Parameters
+ ----------
+ omega : float
+ Frequency of source.
+ x0 : (3,) array_like
+ Position of 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.
+ 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 * rho0 * c)
+ return _util.XyzComponents([i * o / r**2 for o in offset])
+
+
+def point_dipole(omega, x0, n0, grid, *, c=None):
+ r"""Point source with dipole characteristics.
+
+ Parameters
+ ----------
+ omega : float
+ Frequency of source.
+ x0 : (3,) array_like
+ Position of source.
+ n0 : (3,) array_like
+ Normal vector (direction) of dipole.
+ 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
+ -----
+ .. math::
+
+ 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
+ --------
+ .. plot::
+ :context: close-figs
+
+ n0 = 0, 1, 0
+ p = sfs.fd.source.point_dipole(omega, x0, n0, grid)
+ sfs.plot2d.amplitude(p, grid)
+ 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)
+
+ 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)
+
+
+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
+ ----------
+ omega : float
+ Frequency of source.
+ x0 : (3,) array_like
+ Position of source.
+ grid : triple of array_like
+ The grid that is used for the sound field calculations.
+ See `sfs.util.xyz_grid()`.
+ L : (3,) array_like
+ Dimensionons of the rectangular room.
+ N : (3,) array_like or int, optional
+ For all three spatial dimensions per dimension maximum order or
+ list of orders. A scalar applies to all three dimensions. If no
+ order is provided it is approximately determined.
+ deltan : float, optional
+ Absorption coefficient of the walls.
+ c : float, optional
+ Speed of sound.
+
+ Returns
+ -------
+ numpy.ndarray
+ 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)
+
+ if _np.isscalar(N):
+ N = N * _np.ones(3, dtype=int)
+
+ if N is None:
+ N = [None, None, None]
+
+ orders = [0, 0, 0]
+ 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]):
+ # 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]]]
+ ksquared = k**2
+ p = 0
+ 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
+
+
+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
+ ----------
+ omega : float
+ Frequency of source.
+ x0 : (3,) array_like
+ Position of source.
+ grid : triple of array_like
+ The grid that is used for the sound field calculations.
+ See `sfs.util.xyz_grid()`.
+ L : (3,) array_like
+ Dimensionons of the rectangular room.
+ N : (3,) array_like or int, optional
+ Combination of modal orders in the three-spatial dimensions to
+ calculate the sound field for or maximum order for all
+ dimensions. If not given, the maximum modal order is
+ approximately determined and the sound field is computed up to
+ this maximum order.
+ deltan : float, optional
+ Absorption coefficient of the walls.
+ c : float, optional
+ Speed of sound.
+
+ Returns
+ -------
+ `XyzComponents`
+ 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)
+
+ 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))
+ mm = range(Nx)
+ nn = range(Ny)
+ ll = range(Nz)
+ elif _np.isscalar(N):
+ # compute up to a given order
+ mm = range(N)
+ nn = range(N)
+ ll = range(N)
+ else:
+ # compute field for one order combination only
+ mm = [N[0]]
+ 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]]
+ ksquared = k**2
+ vx = 0+0j
+ vy = 0+0j
+ vz = 0+0j
+ 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])
+
+
+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
+ ----------
+ omega : float
+ Frequency of source.
+ x0 : (3,) array_like
+ Position of source.
+ grid : triple of array_like
+ The grid that is used for the sound field calculations.
+ See `sfs.util.xyz_grid()`.
+ L : (3,) array_like
+ Dimensions of the rectangular room.
+ max_order : int
+ Maximum number of reflections for each image source.
+ coeffs : (6,) array_like, optional
+ Reflection coeffecients of the walls.
+ If not given, the reflection coefficients are set to one.
+ c : float, optional
+ Speed of sound.
+
+ Returns
+ -------
+ numpy.ndarray
+ Sound pressure at positions given by *grid*.
+
+ """
+ if coeffs is None:
+ coeffs = _np.ones(6)
+
+ 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'):
+ p += strength * point(omega, position, grid, c=c)
+
+ return p
+
+
+def line(omega, x0, grid, *, c=None):
+ r"""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
+ -------
+ numpy.ndarray
+ Sound pressure at positions given by *grid*.
+
+ Notes
+ -----
+ .. math::
+
+ G(\x-\x_0,\w) = -\frac{\i}{4} \Hankel{2}{0}{\wc|\x-\x_0|}
+
+ Examples
+ --------
+ .. plot::
+ :context: close-figs
+
+ p = sfs.fd.source.line(omega, x0, grid)
+ sfs.plot2d.amplitude(p, grid)
+ plt.title("Line Source at {} m".format(x0[:2]))
+
+ Normalization ...
+
+ .. plot::
+ :context: close-figs
+
+ sfs.plot2d.amplitude(p * normalization_line, grid,
+ colorbar_kwargs=dict(label="p / Pa"))
+ 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)
+
+ r = _np.linalg.norm(grid[:2] - x0)
+ p = -1j/4 * _hankel2_0(k * r)
+ return _duplicate_zdirection(p, grid)
+
+
+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`
+ Particle velocity at positions given by *grid*.
+
+ Examples
+ --------
+ The particle velocity can be plotted on top of the sound pressure:
+
+ .. plot::
+ :context: close-figs
+
+ v = sfs.fd.source.line_velocity(omega, x0, vgrid)
+ sfs.plot2d.amplitude(p * normalization_line, grid)
+ sfs.plot2d.vectors(v * normalization_line, vgrid)
+ plt.title("Sound Pressure and Particle Velocity")
+
+ """
+ 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 * 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]))
+
+ return _util.XyzComponents([_duplicate_zdirection(vi, grid) for vi in v])
+
+
+def line_dipole(omega, x0, n0, grid, *, c=None):
+ r"""Line source with dipole characteristics 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.
+ 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
+ -----
+ .. math::
+
+ 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)
+ dx = grid[:2] - x0
+
+ 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_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.
+
+ :cite:`Moser2012`, eq.(10.18/19)
+
+ Parameters
+ ----------
+ omega : float
+ Angular frequency.
+ x0 : (3,) array_like
+ Position of line source.
+ grid : triple of array_like
+ The grid that is used for the sound field calculations.
+ See `sfs.util.xyz_grid()`.
+ 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
+ -------
+ numpy.ndarray
+ Complex pressure at grid positions.
+
+ """
+ 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)
+
+ 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)
+
+ 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
+
+ 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):
+ 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)
+ p[~idxr & idxa] = p[~idxr & idxa] + f[~idxr & idxa] * \
+ _special.jn(nu, k*r_s) * _special.hankel2(nu, k*r[~idxr & idxa])
+
+ p = p * -1j * _np.pi / alpha
+
+ pl = line(omega, x0, grid, c=c)
+ p[~idxa] = pl[~idxa]
+
+ return p
+
+
+def plane(omega, x0, n0, grid, *, c=None):
+ r"""Plane wave.
+
+ Parameters
+ ----------
+ omega : float
+ Frequency of plane wave.
+ x0 : (3,) array_like
+ Position of plane wave.
+ n0 : (3,) array_like
+ Normal vector (direction) of plane wave.
+ 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
+ -----
+ .. math::
+
+ G(\x,\w) = \e{-\i\wc\n\x}
+
+ Examples
+ --------
+ .. plot::
+ :context: close-figs
+
+ direction = 45 # degree
+ n0 = sfs.util.direction_vector(np.radians(direction))
+ p = sfs.fd.source.plane(omega, x0, n0, grid)
+ sfs.plot2d.amplitude(p, grid, colorbar_kwargs=dict(label="p / Pa"))
+ 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))
+
+
+def plane_velocity(omega, x0, n0, grid, *, c=None, rho0=None):
+ r"""Velocity of a plane wave.
+
+ Parameters
+ ----------
+ omega : float
+ Frequency of plane wave.
+ x0 : (3,) array_like
+ Position of plane wave.
+ n0 : (3,) array_like
+ Normal vector (direction) of plane wave.
+ 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.
+ rho0 : float, optional
+ Static density of air.
+
+ Returns
+ -------
+ `XyzComponents`
+ Particle velocity at positions given by *grid*.
+
+ Notes
+ -----
+ .. math::
+
+ V(\x,\w) = \frac{1}{\rho c} \e{-\i\wc\n\x} \n
+
+ Examples
+ --------
+ The particle velocity can be plotted on top of the sound pressure:
+
+ .. plot::
+ :context: close-figs
+
+ v = sfs.fd.source.plane_velocity(omega, x0, n0, vgrid)
+ sfs.plot2d.amplitude(p, grid)
+ sfs.plot2d.vectors(v, vgrid)
+ plt.title("Sound Pressure and Particle Velocity")
+
+ """
+ if c is None:
+ c = _default.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, rho0=None):
+ r"""Averaged intensity of a plane wave.
+
+ Parameters
+ ----------
+ omega : float
+ Frequency of plane wave.
+ x0 : (3,) array_like
+ Position of plane wave.
+ n0 : (3,) array_like
+ Normal vector (direction) of plane wave.
+ 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.
+ rho0 : float, optional
+ Static density of air.
+
+ Returns
+ -------
+ `XyzComponents`
+ Averaged intensity at positions given by *grid*.
+
+ Notes
+ -----
+ .. math::
+
+ I(\x,\w) = \frac{1}{2\rho c} \n
+
+ """
+ if c is None:
+ c = _default.c
+ if rho0 is None:
+ rho0 = _default.rho0
+ i = 1 / (2 * rho0 * c)
+ 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.fd.source.pulsating_sphere(omega, x0, radius, amplitude, grid)
+ sfs.plot2d.amplitude(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.fd.source.pulsating_sphere_velocity(omega, x0, radius, amplitude, vgrid)
+ sfs.plot2d.amplitude(p, grid)
+ sfs.plot2d.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
+ if len(gridshape) > 2:
+ return _np.tile(p, [1, 1, gridshape[2]])
+ else:
+ return p
+
+
+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)
diff --git a/sfs/fd/wfs.py b/sfs/fd/wfs.py
new file mode 100644
index 0000000..44fb2c6
--- /dev/null
+++ b/sfs/fd/wfs.py
@@ -0,0 +1,707 @@
+"""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.fd.synthesize(d, selection, array, secondary_source, grid=grid)
+ sfs.plot2d.amplitude(p, grid)
+ 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
+
+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):
+ 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.fd.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.fd.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.fd.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.fd.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 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 : (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
+ 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.fd.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.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)
+
+ """
+ x0 = _util.asarray_of_rows(x0)
+ n0 = _util.asarray_of_rows(n0)
+ xs = _util.asarray_1d(xs)
+ xref = _util.asarray_of_rows(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
+ 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.
+ 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.fd.synthesize()`.
+
+ 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|}
+ \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}|}
+
+ 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.fd.wfs.point_25d_legacy(
+ omega, array.x, array.n, xs)
+ normalize_gain = 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
+ 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)
+
+
+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.fd.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.fd.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.fd.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.fd.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.fd.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.fd.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.fd.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.fd.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.fd.synthesize()`.
+
+ Notes
+ -----
+ .. math::
+
+ D(\x_0,\w) = \e{-\i\wc\scalarprod{\n}{\x_0}}
+
+ Examples
+ --------
+ .. plot::
+ :context: close-figs
+
+ d, selection, secondary_source = sfs.fd.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=c)
+ d += selection * figure[n, m] * d_component
+
+ return d, _util.source_selection_all(len(d)), secondary_source
diff --git a/sfs/mono/__init__.py b/sfs/mono/__init__.py
deleted file mode 100644
index 8c09fd8..0000000
--- a/sfs/mono/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""Submodules for monochromatic sound fields."""
-from . import drivingfunction
-from . import source
-from . import synthesized
-from . import soundfigure
diff --git a/sfs/mono/drivingfunction.py b/sfs/mono/drivingfunction.py
deleted file mode 100644
index 06175c9..0000000
--- a/sfs/mono/drivingfunction.py
+++ /dev/null
@@ -1,674 +0,0 @@
-"""Compute driving functions for various systems.
-
-.. include:: math-definitions.rst
-
-"""
-
-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 defs
-
-
-def wfs_2d_line(omega, x0, n0, xs, c=None):
- """Line source by 2-dimensional WFS.
-
- ::
-
- D(x0,k) = j/2 k (x0-xs) n0 / |x0-xs| * H1(k |x0-xs|)
-
- """
- 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)
- return -1j/2 * k * inner1d(ds, n0) / r * hankel2(1, k * r)
-
-
-def _wfs_point(omega, x0, n0, xs, c=None):
- """Point source by two- or three-dimensional WFS.
-
- ::
-
- (x0-xs) n0
- D(x0,k) = j k ------------- e^(-j k |x0-xs|)
- |x0-xs|^(3/2)
-
- """
- 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)
- return 1j * k * inner1d(ds, n0) / r ** (3 / 2) * np.exp(-1j * k * r)
-
-
-wfs_2d_point = _wfs_point
-
-
-def wfs_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None):
- """Point source by 2.5-dimensional WFS.
-
- ::
-
- ____________ (x0-xs) n0
- D(x0,k) = \|j k |xref-x0| ------------- e^(-j k |x0-xs|)
- |x0-xs|^(3/2)
-
- """
- 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)
-
- 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)
-
-
-wfs_3d_point = _wfs_point
-
-
-def _wfs_plane(omega, x0, n0, n=[0, 1, 0], c=None):
- """Plane wave by two- or three-dimensional WFS.
-
- Eq.(17) from :cite:`Spors2008`::
-
- D(x0,k) = j k n n0 e^(-j k n x0)
-
- """
- 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))
-
-
-wfs_2d_plane = _wfs_plane
-
-
-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.
-
- ::
-
- ____________
- D_2.5D(x0,w) = \|j k |xref-x0| n n0 e^(-j k n x0)
-
- """
- 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 wfs_25d_preeq(omega, omalias, c) * \
- np.sqrt(2*np.pi * np.linalg.norm(xref - x0)) * \
- np.inner(n, n0) * np.exp(-1j * k * np.inner(n, x0))
-
-
-wfs_3d_plane = _wfs_plane
-
-
-def _wfs_focused(omega, x0, n0, xs, c=None):
- """Focused source by two- or three-dimensional WFS.
-
- ::
-
- (x0-xs) n0
- D(x0,k) = j k ------------- e^(j k |x0-xs|)
- |x0-xs|^(3/2)
-
- """
- 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)
- return 1j * k * inner1d(ds, n0) / r ** (3 / 2) * np.exp(1j * k * r)
-
-
-wfs_2d_focused = _wfs_focused
-
-
-def wfs_25d_focused(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None):
- """Focused source by 2.5-dimensional WFS.
-
- ::
-
- ____________ (x0-xs) n0
- D(x0,w) = \|j k |xref-x0| ------------- e^(j k |x0-xs|)
- |x0-xs|^(3/2)
-
- """
- 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)
-
- 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)
-
-
-wfs_3d_focused = _wfs_focused
-
-
-def wfs_25d_preeq(omega, omalias, c):
- """Preqeualization for 2.5D WFS."""
- 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):
- """Plane wave by simple delay of secondary sources."""
- 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))
-
-
-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) >= defs.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) >= defs.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) >= defs.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.
-
- .. 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.
-
- """
- 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):
- d += 1j**-m / hankel2(m, k * r0) * np.exp(1j * m * (phi0 - phi))
- return -2j / (np.pi*r0) * d
-
-
-def nfchoa_25d_point(omega, x0, r0, xs, max_order=None, c=None):
- r"""Point source by 2.5-dimensional NFC-HOA.
-
- .. 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.
-
- """
- 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)
- 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)
-
-
-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.
-
- .. 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.
-
- """
- 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):
- d += 1j**-abs(m) / (k * hn2[abs(m)]) * np.exp(1j * m * (phi0 - phi))
- return -2 / r0 * d
-
-
-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) =
-
- """
- 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)
- return - 1j/2 * k * xs[1] / r * hankel2(1, k * r)
-
-
-def sdm_2d_plane(omega, x0, n0, n=[0, 1, 0], c=None):
- """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)::
-
- D(x0,k) = kpw,y * e^(-j*kpw,x*x)
-
- """
- 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])
-
-
-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) =
-
- """
- 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]) * \
- np.exp(-1j*k*n[0]*x0[:, 0])
-
-
-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) =
-
- """
- 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)
- return 1/2 * 1j * k * np.sqrt(xref[1] / (xref[1] - xs[1])) * \
- 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):
- """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.
-
- Derived from :cite:`Spors2016`
-
- 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
- -------
- (N,) numpy.ndarray
- Complex weights of secondary sources.
-
- """
- 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]
-
- 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):
- """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.
-
- Derived from :cite:`Spors2016`
-
- 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
- -------
- (N,) numpy.ndarray
- Complex weights of secondary sources.
-
- """
- 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):
- """Line source by two-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.
-
- Derived from :cite:`Spors2016`
-
- 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
- -------
- (N,) numpy.ndarray
- Complex weights of secondary sources.
-
- """
- 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]
-
- 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):
- """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.
-
- Derived from :cite:`Spors2016`
-
- 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
- -------
- (N,) numpy.ndarray
- Complex weights of secondary sources.
-
- """
- 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)
-
- 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):
- """Line source by two-dimensional ESA for an edge-shaped secondary source
- distribution constisting of dipole line sources.
-
- 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`
-
- 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
- -------
- (N,) numpy.ndarray
- Complex weights of secondary sources.
-
- """
- 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 _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
deleted file mode 100644
index 957b830..0000000
--- a/sfs/mono/soundfigure.py
+++ /dev/null
@@ -1,47 +0,0 @@
-"""Compute driving functions for sound figures."""
-
-import numpy as np
-from .. import util
-from . import drivingfunction
-
-
-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
- a = drivingfunction.source_selection_plane(n0, npw)
- a = a * figure[n, m]
- d += a * drivingfunction.wfs_3d_plane(omega, x0, n0, npw, c)
-
- return d
diff --git a/sfs/mono/source.py b/sfs/mono/source.py
deleted file mode 100644
index 5f70e76..0000000
--- a/sfs/mono/source.py
+++ /dev/null
@@ -1,582 +0,0 @@
-"""Compute the sound field generated by a sound source.
-
-.. plot::
- :context: reset
-
- import sfs
- import numpy as np
- import matplotlib.pyplot as plt
- plt.rcParams['figure.figsize'] = 8, 4.5 # inch
-
- x0 = 1.5, 1, 0
- f = 500 # Hz
- omega = 2 * np.pi * f
-
- normalization_point = 4 * np.pi
- normalization_line = \\
- np.sqrt(8 * np.pi * omega / sfs.defs.c) * np.exp(1j * np.pi / 4)
-
- grid = sfs.util.xyz_grid([-2, 3], [-1, 2], 0, spacing=0.02)
-
- # Grid for vector fields:
- vgrid = sfs.util.xyz_grid([-2, 3], [-1, 2], 0, spacing=0.1)
-
-"""
-
-import itertools
-import numpy as np
-from scipy import special
-from .. import util
-from .. import defs
-
-
-def point(omega, x0, n0, grid, c=None):
- """Point source.
-
- Notes
- -----
- ::
-
- 1 e^(-j w/c |x-x0|)
- G(x-x0, w) = --- -----------------
- 4pi |x-x0|
-
- Examples
- --------
- .. plot::
- :context: close-figs
-
- p = sfs.mono.source.point(omega, x0, None, grid)
- sfs.plot.soundfield(p, grid)
- plt.title("Point Source at {} m".format(x0))
-
- Normalization ...
-
- .. plot::
- :context: close-figs
-
- sfs.plot.soundfield(p * normalization_point, grid,
- colorbar_kwargs=dict(label="p / Pa"))
- 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)
-
- r = np.linalg.norm(grid - x0)
- return 1 / (4*np.pi) * np.exp(-1j * k * r) / r
-
-
-def point_velocity(omega, x0, n0, grid, c=None):
- """Velocity of a point source.
-
- Returns
- -------
- `XyzComponents`
- Particle velocity at positions given by *grid*.
-
- Examples
- --------
- The particle velocity can be plotted on top of the sound pressure:
-
- .. plot::
- :context: close-figs
-
- v = sfs.mono.source.point_velocity(omega, x0, None, vgrid)
- sfs.plot.soundfield(p * normalization_point, grid)
- sfs.plot.vectors(v * normalization_point, vgrid)
- plt.title("Sound Pressure and Particle Velocity")
-
- """
- 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 * defs.c * 1j*k*r)
- return util.XyzComponents([v * o / r for o in offset])
-
-
-def point_dipole(omega, x0, n0, grid, c=None):
- """Point source with dipole characteristics.
-
- Parameters
- ----------
- omega : float
- Frequency of source.
- x0 : (3,) array_like
- Position of source.
- n0 : (3,) array_like
- Normal vector (direction) of dipole.
- 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
- -----
- ::
-
- 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
-
- Examples
- --------
- .. plot::
- :context: close-figs
-
- n0 = 0, 1, 0
- p = sfs.mono.source.point_dipole(omega, x0, n0, grid)
- sfs.plot.soundfield(p, grid)
- 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)
-
- 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)
-
-
-def point_modal(omega, x0, n0, grid, L, N=None, deltan=0, c=None):
- """Point source in a rectangular room using a modal room model.
-
- Parameters
- ----------
- omega : float
- 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()`.
- L : (3,) array_like
- Dimensionons of the rectangular room.
- N : (3,) array_like or int, optional
- For all three spatial dimensions per dimension maximum order or
- list of orders. A scalar applies to all three dimensions. If no
- order is provided it is approximately determined.
- deltan : float, optional
- Absorption coefficient of the walls.
- c : float, optional
- Speed of sound.
-
- Returns
- -------
- numpy.ndarray
- 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)
-
- if np.isscalar(N):
- N = N * np.ones(3, dtype=int)
-
- if N is None:
- N = [None, None, None]
-
- orders = [0, 0, 0]
- 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]):
- # 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]]]
- ksquared = k**2
- p = 0
- for (km0, p0), (km1, p1), (km2, p2) in itertools.product(kmp0, kmp1, kmp2):
- km = km0 + km1 + km2
- p = p + 8 / (ksquared - km) * p0 * p1 * p2
- return p
-
-
-def point_modal_velocity(omega, x0, n0, grid, L, N=None, deltan=0, c=None):
- """Velocity of point source in a rectangular room using a modal room model.
-
- Parameters
- ----------
- omega : float
- 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()`.
- L : (3,) array_like
- Dimensionons of the rectangular room.
- N : (3,) array_like or int, optional
- Combination of modal orders in the three-spatial dimensions to
- calculate the sound field for or maximum order for all
- dimensions. If not given, the maximum modal order is
- approximately determined and the sound field is computed up to
- this maximum order.
- deltan : float, optional
- Absorption coefficient of the walls.
- c : float, optional
- Speed of sound.
-
- Returns
- -------
- `XyzComponents`
- 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)
-
- 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))
- mm = range(Nx)
- nn = range(Ny)
- ll = range(Nz)
- elif np.isscalar(N):
- # compute up to a given order
- mm = range(N)
- nn = range(N)
- ll = range(N)
- else:
- # compute field for one order combination only
- mm = [N[0]]
- 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]]
- 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):
- 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])
-
-
-def point_image_sources(omega, x0, n0, grid, L, max_order, coeffs=None,
- c=None):
- """Point source in a rectangular room using the mirror image source model.
-
- Parameters
- ----------
- omega : float
- 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()`.
- L : (3,) array_like
- Dimensions of the rectangular room.
- max_order : int
- Maximum number of reflections for each image source.
- coeffs : (6,) array_like, optional
- Reflection coeffecients of the walls.
- If not given, the reflection coefficients are set to one.
- c : float, optional
- Speed of sound.
-
- Returns
- -------
- numpy.ndarray
- Sound pressure at positions given by *grid*.
-
- """
- if coeffs is None:
- coeffs = np.ones(6)
-
- 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:
- p += strength * point(omega, position, n0, grid, c)
-
- return p
-
-
-def line(omega, x0, n0, grid, c=None):
- """Line source parallel to the z-axis.
-
- Note: third component of x0 is ignored.
-
- Notes
- -----
- ::
-
- (2)
- G(x-x0, w) = -j/4 H0 (w/c |x-x0|)
-
- Examples
- --------
- .. plot::
- :context: close-figs
-
- p = sfs.mono.source.line(omega, x0, None, grid)
- sfs.plot.soundfield(p, grid)
- plt.title("Line Source at {} m".format(x0[:2]))
-
- Normalization ...
-
- .. plot::
- :context: close-figs
-
- sfs.plot.soundfield(p * normalization_line, grid,
- colorbar_kwargs=dict(label="p / Pa"))
- 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)
-
- r = np.linalg.norm(grid[:2] - x0)
- p = -1j/4 * _hankel2_0(k * r)
- return _duplicate_zdirection(p, grid)
-
-
-def line_velocity(omega, x0, n0, grid, c=None):
- """Velocity of line source parallel to the z-axis.
-
- Returns
- -------
- `XyzComponents`
- Particle velocity at positions given by *grid*.
-
- Examples
- --------
- The particle velocity can be plotted on top of the sound pressure:
-
- .. plot::
- :context: close-figs
-
- v = sfs.mono.source.line_velocity(omega, x0, None, vgrid)
- sfs.plot.soundfield(p * normalization_line, grid)
- sfs.plot.vectors(v * normalization_line, vgrid)
- plt.title("Sound Pressure and Particle Velocity")
-
- """
- 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*defs.c*defs.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]))
-
- return util.XyzComponents([_duplicate_zdirection(vi, grid) for vi in v])
-
-
-def line_dipole(omega, x0, n0, grid, c=None):
- """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)
-
-
- """
- 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
- return _duplicate_zdirection(p, grid)
-
-
-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)
-
- Parameters
- ----------
- omega : float
- Angular frequency.
- x0 : (3,) array_like
- Position of line source.
- grid : triple of array_like
- The grid that is used for the sound field calculations.
- See `sfs.util.xyz_grid()`.
- 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
- -------
- numpy.ndarray
- Complex pressure at grid positions.
-
- """
- 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)
-
- 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)
-
- 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
-
- 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)
- p[idxr & idxa] = p[idxr & idxa] + f[idxr & idxa] * \
- 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])
-
- p = p * -1j*np.pi/alpha
-
- pl = line(omega, x0, None, grid, c=c)
- p[~idxa] = pl[~idxa]
-
- return p
-
-
-def plane(omega, x0, n0, grid, c=None):
- """Plane wave.
-
- Notes
- -----
- ::
-
- G(x, w) = e^(-i w/c n x)
-
- Examples
- --------
- .. plot::
- :context: close-figs
-
- direction = 45 # degree
- n0 = sfs.util.direction_vector(np.radians(direction))
- p = sfs.mono.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))
-
- """
- 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):
- """Velocity of a plane wave.
-
- Notes
- -----
- ::
-
- V(x, w) = 1/(rho c) e^(-i w/c n x) n
-
- Returns
- -------
- `XyzComponents`
- Particle velocity at positions given by *grid*.
-
- Examples
- --------
- The particle velocity can be plotted on top of the sound pressure:
-
- .. plot::
- :context: close-figs
-
- v = sfs.mono.source.plane_velocity(omega, x0, n0, vgrid)
- sfs.plot.soundfield(p, grid)
- sfs.plot.vectors(v, vgrid)
- plt.title("Sound Pressure and Particle Velocity")
-
- """
- v = plane(omega, x0, n0, grid, c=c) / (defs.rho0 * defs.c)
- return util.XyzComponents([v * n for n in n0])
-
-
-def _duplicate_zdirection(p, grid):
- """If necessary, duplicate field in z-direction."""
- gridshape = np.broadcast(*grid).shape
- if len(gridshape) > 2:
- return np.tile(p, [1, 1, gridshape[2]])
- else:
- return p
-
-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)
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/plot.py b/sfs/plot2d.py
similarity index 60%
rename from sfs/plot.py
rename to sfs/plot2d.py
index 05a9af6..efadb9c 100644
--- a/sfs/plot.py
+++ b/sfs/plot2d.py
@@ -1,24 +1,25 @@
-"""Plot sound fields etc."""
-from __future__ import division
-import matplotlib.pyplot as plt
-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 defs
+"""2D plots of sound fields etc."""
+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
- 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)
+ _plt.cm.register_cmap(cmap=cmap)
+
# The 'coolwarm' colormap is based on the paper
# "Diverging Color Maps for Scientific Visualization" by Kenneth Moreland
@@ -35,23 +36,24 @@ 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')
-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)
+ 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
@@ -60,34 +62,46 @@ 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)
+ 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-')
-def secondarysource_2d(x0, n0, grid=None):
- """Simple plot of secondary source locations."""
- x0 = np.asarray(x0)
- n0 = np.asarray(n0)
- ax = plt.gca()
+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()
# 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:
- 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)
-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
@@ -109,76 +123,66 @@ def loudspeaker_2d(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:
- 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(*(
- (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')
+ 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()) &
+ 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, :]
-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
----------
@@ -238,15 +242,15 @@ 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)
- 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:
@@ -281,11 +285,18 @@ 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()
+ ax = _plt.gca()
- im = ax.imshow(np.real(p), cmap=cmap, origin='lower',
- extent=[x.min(), x.max(), y.min(), y.max()],
+ # see https://github.com/matplotlib/matplotlib/issues/10567
+ 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',
+ 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'
@@ -298,11 +309,11 @@ def soundfield(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.
- Takes the same parameters as `sfs.plot.soundfield()`.
+ Takes the same parameters as `sfs.plot2d.amplitude()`.
Other Parameters
----------------
@@ -312,37 +323,67 @@ 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)
- return soundfield(L, grid=grid, xnorm=None, cmap=cmap,
- vmax=vmax, vmin=vmin, **kwargs)
+ 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)
-def particles(x, trim=None, ax=None, xlabel='x (m)', ylabel='y (m)',
- edgecolor='', **kwargs):
- """Plot particle positions as scatter plot"""
- XX, YY = [np.real(c) for c in x[:2]]
+def particles(x, *, trim=None, ax=None, xlabel='x (m)', ylabel='y (m)',
+ 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:
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.scatter(XX, YY, edgecolor=edgecolor, **kwargs)
+ ax = _plt.gca()
if xlabel:
ax.set_xlabel(xlabel)
if ylabel:
ax.set_ylabel(ylabel)
+ return ax.scatter(XX, YY, edgecolors=edgecolors, marker=marker, s=s,
+ **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
@@ -373,28 +414,28 @@ def vectors(v, grid, cmap='blacktransparent', headlength=3, headaxislength=2.5,
: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 / (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,
headaxislength=headaxislength, clim=clim, **kwargs)
-def add_colorbar(im, aspect=20, pad=0.5, **kwargs):
- """Add a vertical color bar to a plot.
+def add_colorbar(im, *, aspect=20, pad=0.5, **kwargs):
+ r"""Add a vertical color bar to a plot.
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
@@ -418,10 +459,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
new file mode 100644
index 0000000..063a1d1
--- /dev/null
+++ b/sfs/plot3d.py
@@ -0,0 +1,14 @@
+"""3D plots of sound fields etc."""
+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))
+ 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/tapering.py b/sfs/tapering.py
index f5d6d6f..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):
@@ -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
@@ -67,7 +67,7 @@ def tukey(active, alpha):
Returns
-------
- (len(active),) numpy.ndarray
+ (len(active),) `numpy.ndarray`
Tapering weights.
Examples
@@ -75,41 +75,41 @@ 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])
"""
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
-def kaiser(active, beta):
+def kaiser(active, *, beta):
"""Kaiser tapering window.
This uses :func:`numpy.kaiser`.
@@ -123,7 +123,7 @@ def kaiser(active, beta):
Returns
-------
- (len(active),) numpy.ndarray
+ (len(active),) `numpy.ndarray`
Tapering weights.
Examples
@@ -131,24 +131,24 @@ 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])
"""
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
new file mode 100644
index 0000000..a786ea5
--- /dev/null
+++ b/sfs/td/__init__.py
@@ -0,0 +1,112 @@
+"""Submodules for broadband sound fields.
+
+.. autosummary::
+ :toctree:
+
+ source
+
+ wfs
+ nfchoa
+
+"""
+import numpy as _np
+
+from . import source
+from .. import array as _array
+from .. import util as _util
+
+
+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, **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
+
+
+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.td.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/td/nfchoa.py b/sfs/td/nfchoa.py
new file mode 100644
index 0000000..87a2e5f
--- /dev/null
+++ b/sfs/td/nfchoa.py
@@ -0,0 +1,517 @@
+"""Compute NFC-HOA 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
+
+ # 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.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)
+
+"""
+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):
+ """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 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::
+
+ 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.
+ 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.td.synthesize()`.
+
+ Examples
+ --------
+ .. plot::
+ :context: close-figs
+
+ delay, weight, sos, phaseshift, selection, secondary_source = \
+ 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)
+
+ """
+ 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, _ = _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(_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):
+ 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.
+ 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.td.synthesize()`.
+
+ Examples
+ --------
+ .. plot::
+ :context: close-figs
+
+ delay, weight, sos, phaseshift, selection, secondary_source = \
+ 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)
+
+ """
+ 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, _ = _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(_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):
+ 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.
+ 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.td.synthesize()`.
+
+ """
+ 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, _ = _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(_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):
+ 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.
+ 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.td.synthesize()`.
+
+ """
+ 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, _ = _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(_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):
+ """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(_sig.sosfilt(sos[0], data), 1), (1, N))
+ for m in range(1, len(sos)):
+ 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):
+ """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(_sig.sosfilt(sos[0], data), 1), (1, N))
+ for m in range(1, len(sos)):
+ 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/time/source.py b/sfs/td/source.py
similarity index 59%
rename from sfs/time/source.py
rename to sfs/td/source.py
index 0101daf..338dfdb 100644
--- a/sfs/time/source.py
+++ b/sfs/td/source.py
@@ -4,12 +4,29 @@
.. 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
+ fs = 44100
+ signal = unit_impulse(512), fs
+
+ grid = sfs.util.xyz_grid([-2, 3], [-1, 2], 0, spacing=0.02)
+
"""
+import numpy as _np
-from __future__ import division
-import numpy as np
-from .. import util
-from .. import defs
+from .. import default as _default
+from .. import util as _util
def point(xs, signal, observation_time, grid, c=None):
@@ -46,21 +63,33 @@ 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.td.source.point(xs, signal, ts, grid)
+ 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 = defs.c
- r = np.linalg.norm(grid - xs)
- # evaluate g over grid
- weights = 1 / (4 * np.pi * r)
+ 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)
delays = r / c
base_time = observation_time - signal_offset
- return weights * 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'):
+ return weights * points_at_time
def point_image_sources(x0, signal, observation_time, grid, L, max_order,
@@ -95,12 +124,25 @@ 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.td.source.point_image_sources(
+ xs, signal, 1.5 * ts, grid, room, order, coeffs)
+ sfs.plot2d.level(p, grid)
+
"""
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
new file mode 100644
index 0000000..3b59301
--- /dev/null
+++ b/sfs/td/wfs.py
@@ -0,0 +1,414 @@
+"""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.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)
+
+"""
+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):
+ 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.td.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) =
+ 2 g_0 \scalarprod{n}{n_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.
+
+ See :sfs:`d_wfs/#equation-td-wfs-plane-25d`
+
+ Examples
+ --------
+ .. plot::
+ :context: close-figs
+
+ delays, weights, selection, secondary_source = \
+ sfs.td.wfs.plane_25d(array.x, array.n, npw)
+ d = sfs.td.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"""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*_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
+ ----------
+ 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.td.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) =
+ \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}} \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::
+ :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_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.td.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) =
+ \frac{g_0 \scalarprod{(x_0 - x_s)}{n_0}}
+ {|x_0 - x_s|^{3/2}}
+ \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-focused-25d`
+
+ Examples
+ --------
+ .. plot::
+ :context: close-figs
+
+ delays, weights, selection, secondary_source = \
+ 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)
+
+ """
+ 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)
diff --git a/sfs/time/__init__.py b/sfs/time/__init__.py
deleted file mode 100644
index f4f826a..0000000
--- a/sfs/time/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from . import drivingfunction
-from . import source
-from . import soundfield
\ No newline at end of file
diff --git a/sfs/time/drivingfunction.py b/sfs/time/drivingfunction.py
deleted file mode 100644
index 04922bd..0000000
--- a/sfs/time/drivingfunction.py
+++ /dev/null
@@ -1,257 +0,0 @@
-"""Compute time based driving functions for various systems.
-
-.. 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
-from .. import util
-
-
-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.
-
- 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
-
- """
- if c is None:
- c = defs.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)
- return delays, weights
-
-
-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.
-
- 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
-
- """
- if c is None:
- c = defs.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))
- return delays, weights
-
-
-def wfs_25d_focused(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.
-
- 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
-
- """
- if c is None:
- c = defs.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))
- return delays, weights
-
-
-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)
diff --git a/sfs/time/soundfield.py b/sfs/time/soundfield.py
deleted file mode 100644
index ff6d32d..0000000
--- a/sfs/time/soundfield.py
+++ /dev/null
@@ -1,56 +0,0 @@
-"""Compute sound field."""
-
-from __future__ import division
-from .. import util
-from .. import defs
-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 = defs.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/sfs/util.py b/sfs/util.py
index ed3a3a8..c15358f 100644
--- a/sfs/util.py
+++ b/sfs/util.py
@@ -4,11 +4,11 @@
"""
-from __future__ import division
import collections
import numpy as np
+from numpy.core.umath_tests import inner1d
from scipy.special import spherical_jn, spherical_yn
-from . import defs
+from . import default
def rotation_matrix(n1, n2):
@@ -21,7 +21,7 @@ def rotation_matrix(n1, n2):
Returns
-------
- (3, 3) numpy.ndarray
+ (3, 3) `numpy.ndarray`
Rotation matrix.
"""
@@ -47,7 +47,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
@@ -72,23 +72,23 @@ 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
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
@@ -101,7 +101,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
----------
@@ -114,12 +114,13 @@ 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
- Elevation angle in radiants (with 0 denoting North pole)
- r : float or array_like
+ beta : float or `numpy.ndarray`
+ Colatitude angle in radiants (with 0 denoting North pole)
+ r : float or `numpy.ndarray`
Radius
+
"""
r = np.sqrt(x**2 + y**2 + z**2)
alpha = np.arctan2(y, x)
@@ -159,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
@@ -215,8 +216,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
@@ -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`,
@@ -251,7 +252,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`.
"""
@@ -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
@@ -338,18 +339,7 @@ def normalize_vector(x):
return x / np.linalg.norm(x)
-def displacement(v, omega):
- """Particle displacement
-
- .. math::
-
- d(x, t) = \int_0^t v(x, t) dt
-
- """
- return as_xyz_components(v) / (1j * omega)
-
-
-def db(x, power=False):
+def db(x, *, power=False):
"""Convert *x* to decibel.
Parameters
@@ -362,14 +352,14 @@ 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):
"""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
@@ -431,7 +421,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):
@@ -477,7 +467,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`.
@@ -507,9 +497,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.
"""
@@ -544,7 +534,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::
@@ -563,3 +553,96 @@ 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)
+
+
+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 :cite:`Ahrens2012` as
+
+ .. math::
+ \mathtt{max\_order} =
+ \begin{cases}
+ N/2 - 1 & \text{even}\;N \\
+ (N-1)/2 & \text{odd}\;N,
+ \end{cases}
+
+ which is equivalent to
+
+ .. math::
+ \mathtt{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::
+ \mathtt{max\_order} = \lfloor \sqrt{N} \rfloor - 1.
+
+ Parameters
+ ----------
+ N : int
+ Number of secondary sources.
+
+ """
+ return int(np.sqrt(N) - 1)
diff --git a/tests/test_array.py b/tests/test_array.py
index 789c7a4..5100f41 100644
--- a/tests/test_array.py
+++ b/tests/test_array.py
@@ -11,31 +11,27 @@ def vectortypes(*coeffs):
np.array(coeffs),
np.array(coeffs).reshape(1, -1),
np.array(coeffs).reshape(-1, 1),
- np.matrix(coeffs),
- np.matrix(coeffs).transpose(),
]
def vector_id(vector):
- if isinstance(vector, np.matrix):
- return 'matrix, shape=' + repr(vector.shape)
- elif isinstance(vector, np.ndarray):
+ if isinstance(vector, np.ndarray):
return 'array, shape=' + repr(vector.shape)
return type(vector).__name__
@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],
diff --git a/tests/test_util.py b/tests/test_util.py
new file mode 100644
index 0000000..6eb7a92
--- /dev/null
+++ b/tests/test_util.py
@@ -0,0 +1,70 @@
+import numpy as np
+from numpy.testing import assert_allclose
+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), (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), (-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), (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), (-3 / 4 * np.pi, np.arccos(-1 / np.sqrt(3)), np.sqrt(3))),
+]
+
+
+@pytest.mark.parametrize('coord, polar', cart_sph_data)
+def test_cart2sph(coord, polar):
+ x, y, z = coord
+ a = sfs.util.cart2sph(x, y, z)
+ assert_allclose(a, polar)
+
+
+@pytest.mark.parametrize('coord, polar', cart_sph_data)
+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),
+ (0.5, -3.01029995663981),
+ (1, 0),
+ (2, 3.01029995663981),
+ (10, 10),
+]
+
+
+@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, power_db', db_data)
+def test_db_power(linear, power_db):
+ d = sfs.util.db(linear, power=True)
+ assert_allclose(d, power_db)