├── .editorconfig
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── CONTRIBUTING.rst
├── LICENSE
├── MANIFEST.in
├── NEWS.rst
├── README.rst
├── binder
├── README
├── apt.txt
├── postBuild
└── requirements.txt
├── data
└── arrays
│ ├── example_array_4LS_2D.csv
│ ├── example_array_6LS_3D.txt
│ ├── wfs_university_rostock_2015.csv
│ └── wfs_university_rostock_2018.csv
├── doc
├── README
├── _static
│ └── css
│ │ └── title.css
├── _template
│ └── layout.html
├── api.rst
├── conf.py
├── contributing.rst
├── example-python-scripts.rst
├── examples.rst
├── examples
│ ├── animations-pulsating-sphere.ipynb
│ ├── animations_pulsating_sphere.py
│ ├── figures
│ │ ├── circle.png
│ │ ├── cross.png
│ │ ├── rect.png
│ │ └── tree.png
│ ├── horizontal_plane_arrays.py
│ ├── ipython_kernel_config.py
│ ├── mirror-image-source-model.ipynb
│ ├── modal-room-acoustics.ipynb
│ ├── plot_particle_density.py
│ ├── run_all.py
│ ├── sound-field-synthesis.ipynb
│ ├── soundfigures.py
│ ├── time_domain.py
│ └── time_domain_nfchoa.py
├── index.rst
├── installation.rst
├── math-definitions.rst
├── readthedocs-environment.yml
├── references.bib
├── references.rst
├── requirements.txt
└── version-history.rst
├── examples
├── readthedocs.yml
├── setup.cfg
├── setup.py
├── sfs
├── __init__.py
├── array.py
├── fd
│ ├── __init__.py
│ ├── esa.py
│ ├── nfchoa.py
│ ├── sdm.py
│ ├── source.py
│ └── wfs.py
├── plot2d.py
├── plot3d.py
├── tapering.py
├── td
│ ├── __init__.py
│ ├── nfchoa.py
│ ├── source.py
│ └── wfs.py
└── util.py
└── tests
├── requirements.txt
├── test_array.py
└── test_util.py
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | end_of_line = lf
7 | charset = utf-8
8 | max_line_length = 80
9 | indent_style = space
10 | indent_size = 4
11 | insert_final_newline = true
12 | trim_trailing_whitespace = true
13 |
14 | [*.py]
15 | max_line_length = 79
16 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ${{ matrix.os }}
9 | strategy:
10 | matrix:
11 | os: [ubuntu-latest, macOS-latest, windows-latest]
12 | python-version: [3.6, 3.7, 3.8]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Set up Python ${{ matrix.python-version }}
17 | uses: actions/setup-python@v2
18 | with:
19 | python-version: ${{ matrix.python-version }}
20 | - name: Prepare Ubuntu
21 | run: |
22 | sudo apt-get update
23 | sudo apt-get install --no-install-recommends -y pandoc ffmpeg
24 | if: matrix.os == 'ubuntu-latest'
25 | - name: Prepare OSX
26 | run: brew install pandoc ffmpeg
27 | if: matrix.os == 'macOS-latest'
28 | - name: prepare Windows
29 | run: choco install pandoc ffmpeg
30 | if: matrix.os == 'windows-latest'
31 | - name: Install dependencies
32 | run: |
33 | python -V
34 | python -m pip install --upgrade pip
35 | python -m pip install .
36 | python -m pip install -r tests/requirements.txt
37 | python -m pip install -r doc/requirements.txt
38 | # This is needed in example scripts:
39 | python -m pip install pillow
40 | - name: Test
41 | run: python -m pytest
42 | - name: Test examples
43 | run: python doc/examples/run_all.py
44 | - name: Test documentation
45 | run: python -m sphinx doc/ _build/ -b doctest
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | __pycache__/
3 | build/
4 | dist/
5 | .eggs/
6 | sfs.egg-info/
7 |
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | Contributing
2 | ------------
3 |
4 | If you find errors, omissions, inconsistencies or other things that need
5 | improvement, please create an issue or a pull request at
6 | https://github.com/sfstoolbox/sfs-python/.
7 | Contributions are always welcome!
8 |
9 | Development Installation
10 | ^^^^^^^^^^^^^^^^^^^^^^^^
11 |
12 | Instead of pip-installing the latest release from PyPI_, you should get the
13 | newest development version from Github_::
14 |
15 | git clone https://github.com/sfstoolbox/sfs-python.git
16 | cd sfs-python
17 | python3 -m pip install --user -e .
18 |
19 | ... where ``-e`` stands for ``--editable``.
20 |
21 | This way, your installation always stays up-to-date, even if you pull new
22 | changes from the Github repository.
23 |
24 | .. _PyPI: https://pypi.org/project/sfs/
25 | .. _Github: https://github.com/sfstoolbox/sfs-python/
26 |
27 |
28 | Building the Documentation
29 | ^^^^^^^^^^^^^^^^^^^^^^^^^^
30 |
31 | If you make changes to the documentation, you can re-create the HTML pages
32 | using Sphinx_.
33 | You can install it and a few other necessary packages with::
34 |
35 | python3 -m pip install -r doc/requirements.txt --user
36 |
37 | To create the HTML pages, use::
38 |
39 | python3 setup.py build_sphinx
40 |
41 | The generated files will be available in the directory ``build/sphinx/html/``.
42 |
43 | To create the EPUB file, use::
44 |
45 | python3 setup.py build_sphinx -b epub
46 |
47 | The generated EPUB file will be available in the directory
48 | ``build/sphinx/epub/``.
49 |
50 | To create the PDF file, use::
51 |
52 | python3 setup.py build_sphinx -b latex
53 |
54 | Afterwards go to the folder ``build/sphinx/latex/`` and run LaTeX to create the
55 | PDF file. If you don’t know how to create a PDF file from the LaTeX output, you
56 | should have a look at Latexmk_ (see also this `Latexmk tutorial`_).
57 |
58 | It is also possible to automatically check if all links are still valid::
59 |
60 | python3 setup.py build_sphinx -b linkcheck
61 |
62 | .. _Sphinx: http://sphinx-doc.org/
63 | .. _Latexmk: http://personal.psu.edu/jcc8/software/latexmk-jcc/
64 | .. _Latexmk tutorial: https://mg.readthedocs.io/latexmk.html
65 |
66 | Running the Tests
67 | ^^^^^^^^^^^^^^^^^
68 |
69 | You'll need pytest_ for that.
70 | It can be installed with::
71 |
72 | python3 -m pip install -r tests/requirements.txt --user
73 |
74 | To execute the tests, simply run::
75 |
76 | python3 -m pytest
77 |
78 | .. _pytest: https://pytest.org/
79 |
80 | Creating a New Release
81 | ^^^^^^^^^^^^^^^^^^^^^^
82 |
83 | New releases are made using the following steps:
84 |
85 | #. Bump version number in ``sfs/__init__.py``
86 | #. Update ``NEWS.rst``
87 | #. Commit those changes as "Release x.y.z"
88 | #. Create an (annotated) tag with ``git tag -a x.y.z``
89 | #. Clear the ``dist/`` directory
90 | #. Create a source distribution with ``python3 setup.py sdist``
91 | #. Create a wheel distribution with ``python3 setup.py bdist_wheel``
92 | #. Check that both files have the correct content
93 | #. Upload them to PyPI_ with twine_: ``python3 -m twine upload dist/*``
94 | #. Push the commit and the tag to Github and `add release notes`_ containing a
95 | link to PyPI and the bullet points from ``NEWS.rst``
96 | #. Check that the new release was built correctly on RTD_
97 | and select the new release as default version
98 |
99 | .. _twine: https://twine.readthedocs.io/
100 | .. _add release notes: https://github.com/sfstoolbox/sfs-python/tags
101 | .. _RTD: https://readthedocs.org/projects/sfs-python/builds/
102 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014-2019 SFS Toolbox Developers
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include *.rst
3 | include doc/requirements.txt
4 | recursive-include doc *.rst *.py
5 | recursive-include tests *.py
6 | include doc/examples/*.ipynb
7 | include doc/examples/figures/*.png
8 | recursive-include data *
9 |
--------------------------------------------------------------------------------
/NEWS.rst:
--------------------------------------------------------------------------------
1 | Version History
2 | ===============
3 |
4 |
5 | Version 0.6.2 (2021-06-05):
6 | * build doc fix, use sphinx4, mathjax2, html_css_files
7 |
8 | Version 0.6.1 (2021-06-05):
9 | * New default driving function for `sfs.td.wfs.point_25d()` for reference curve
10 |
11 | Version 0.6.0 (2020-12-01):
12 | * New function `sfs.fd.source.line_bandlimited()` computing the sound field of a spatially bandlimited line source
13 | * Drop support for Python 3.5
14 |
15 | Version 0.5.0 (2019-03-18):
16 | * Switching to separate `sfs.plot2d` and `sfs.plot3d` for plotting functions
17 | * Move `sfs.util.displacement()` to `sfs.fd.displacement()`
18 | * Switch to keyword only arguments
19 | * New default driving function for `sfs.fd.wfs.point_25d()`
20 | * New driving function syntax, e.g. `sfs.fd.wfs.point_25d()`
21 | * Example for the sound field of a pulsating sphere
22 | * Add time domain NFC-HOA driving functions `sfs.td.nfchoa`
23 | * `sfs.fd.synthesize()`, `sfs.td.synthesize()` for soundfield superposition
24 | * Change `sfs.mono` to `sfs.fd` and `sfs.time` to `sfs.td`
25 | * Move source selection helpers to `sfs.util`
26 | * Use `sfs.default` object instead of `sfs.defs` submodule
27 | * Drop support for legacy Python 2.7
28 |
29 | Version 0.4.0 (2018-03-14):
30 | * Driving functions in time domain for a plane wave, point source, and
31 | focused source
32 | * Image source model for a point source in a rectangular room
33 | * `sfs.util.DelayedSignal` class and `sfs.util.as_delayed_signal()`
34 | * Improvements to the documentation
35 | * Start using Jupyter notebooks for examples in documentation
36 | * Spherical Hankel function as `sfs.util.spherical_hn2()`
37 | * Use `scipy.special.spherical_jn`, `scipy.special.spherical_yn` instead of
38 | `scipy.special.sph_jnyn`
39 | * Generalization of the modal order argument in `sfs.mono.source.point_modal()`
40 | * Rename `sfs.util.normal_vector()` to `sfs.util.normalize_vector()`
41 | * Add parameter ``max_order`` to NFCHOA driving functions
42 | * Add ``beta`` parameter to Kaiser tapering window
43 | * Fix clipping problem of sound field plots with matplotlib 2.1
44 | * Fix elevation in `sfs.util.cart2sph()`
45 | * Fix `sfs.tapering.tukey()` for ``alpha=1``
46 |
47 | Version 0.3.1 (2016-04-08):
48 | * Fixed metadata of release
49 |
50 | Version 0.3.0 (2016-04-08):
51 | * Dirichlet Green's function for the scattering of a line source at an edge
52 | * Driving functions for the synthesis of various virtual source types with
53 | edge-shaped arrays by the equivalent scattering appoach
54 | * Driving functions for the synthesis of focused sources by WFS
55 |
56 | Version 0.2.0 (2015-12-11):
57 | * Ability to calculate and plot particle velocity and displacement fields
58 | * Several function name and parameter name changes
59 |
60 | Version 0.1.1 (2015-10-08):
61 | * Fix missing `sfs.mono` subpackage in PyPI packages
62 |
63 | Version 0.1.0 (2015-09-22):
64 | Initial release.
65 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Sound Field Synthesis (SFS) Toolbox for Python
2 | ==============================================
3 |
4 | A Python library for creating numercial simulations of sound field synthesis
5 | methods like Wave Field Synthesis (WFS) or Near-Field Compensated Higher Order
6 | Ambisonics (NFC-HOA).
7 |
8 | Documentation:
9 | https://sfs-python.readthedocs.io/
10 |
11 | Source code and issue tracker:
12 | https://github.com/sfstoolbox/sfs-python/
13 |
14 | License:
15 | MIT -- see the file ``LICENSE`` for details.
16 |
17 | Quick start:
18 | * Install Python 3, NumPy, SciPy and Matplotlib
19 | * ``python3 -m pip install sfs --user``
20 | * Check out the examples in the documentation
21 |
22 | More information about the underlying theory can be found at
23 | https://sfs.readthedocs.io/.
24 | There is also a Sound Field Synthesis Toolbox for Octave/Matlab, see
25 | https://sfs-matlab.readthedocs.io/.
26 |
--------------------------------------------------------------------------------
/binder/README:
--------------------------------------------------------------------------------
1 | This directory holds configuration files for https://mybinder.org/.
2 |
3 | The SFS Toolbox examples can be accessed with this link:
4 | https://mybinder.org/v2/gh/sfstoolbox/sfs-python/master?filepath=doc/examples
5 |
6 | To check out a different version, just replace "master" with the desired
7 | branch/tag name or commit hash.
8 |
--------------------------------------------------------------------------------
/binder/apt.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sfstoolbox/sfs-python/043e9dd0f6aae5eeebbed8357968a2669d29fc40/binder/apt.txt
--------------------------------------------------------------------------------
/binder/postBuild:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | python setup.py develop
6 |
--------------------------------------------------------------------------------
/binder/requirements.txt:
--------------------------------------------------------------------------------
1 | numpy
2 | scipy
3 | matplotlib>=1.5
4 |
--------------------------------------------------------------------------------
/data/arrays/example_array_4LS_2D.csv:
--------------------------------------------------------------------------------
1 | 1,0,0,-1,0,0,1
2 | 0,1,0,0,-1,0,1
3 | -1,0,0,1,0,0,1
4 | 0,-1,0,0,1,0,1
--------------------------------------------------------------------------------
/data/arrays/example_array_6LS_3D.txt:
--------------------------------------------------------------------------------
1 | 1 0 0 1
2 | -1 0 0 1
3 | 0 1 0 1
4 | 0 -1 0 1
5 | 0 0 1 1
6 | 0 0 -1 1
--------------------------------------------------------------------------------
/data/arrays/wfs_university_rostock_2015.csv:
--------------------------------------------------------------------------------
1 | 1.88,0.1275,0,-1,0,0,1
2 | 1.88,0.31,0,-1,0,0,1
3 | 1.88,0.54,0,-1,0,0,1
4 | 1.88,0.7725,0,-1,0,0,1
5 | 1.88,1.02,0,-1,0,0,1
6 | 1.88,1.27,0,-1,0,0,1
7 | 1.88,1.4975,0,-1,0,0,1
8 | 1.88,1.6775,0,-1,0,0,1
9 | 1.685,1.88,0,0,-1,0,1
10 | 1.495,1.88,0,0,-1,0,1
11 | 1.2525,1.88,0,0,-1,0,1
12 | 1.02,1.88,0,0,-1,0,1
13 | 0.7725,1.88,0,0,-1,0,1
14 | 0.5475,1.88,0,0,-1,0,1
15 | 0.3025,1.88,0,0,-1,0,1
16 | 0.0575,1.88,0,0,-1,0,1
17 | -0.13,1.88,0,0,-1,0,1
18 | -0.315,1.88,0,0,-1,0,1
19 | -0.5375,1.88,0,0,-1,0,1
20 | -0.7725,1.88,0,0,-1,0,1
21 | -1.0175,1.88,0,0,-1,0,1
22 | -1.27,1.88,0,0,-1,0,1
23 | -1.4975,1.88,0,0,-1,0,1
24 | -1.69,1.88,0,0,-1,0,1
25 | -1.88,1.6875,0,1,0,0,1
26 | -1.88,1.5,0,1,0,0,1
27 | -1.88,1.2525,0,1,0,0,1
28 | -1.88,1.02,0,1,0,0,1
29 | -1.88,0.775,0,1,0,0,1
30 | -1.88,0.55,0,1,0,0,1
31 | -1.88,0.305,0,1,0,0,1
32 | -1.88,0.0625,0,1,0,0,1
33 | -1.88,-0.13,0,1,0,0,1
34 | -1.88,-0.3125,0,1,0,0,1
35 | -1.88,-0.5325,0,1,0,0,1
36 | -1.88,-0.7625,0,1,0,0,1
37 | -1.88,-1.0125,0,1,0,0,1
38 | -1.88,-1.2625,0,1,0,0,1
39 | -1.88,-1.5,0,1,0,0,1
40 | -1.88,-1.69,0,1,0,0,1
41 | -1.6925,-1.88,0,0,1,0,1
42 | -1.505,-1.88,0,0,1,0,1
43 | -1.2625,-1.88,0,0,1,0,1
44 | -1.025,-1.88,0,0,1,0,1
45 | -0.785,-1.88,0,0,1,0,1
46 | -0.555,-1.88,0,0,1,0,1
47 | -0.3125,-1.88,0,0,1,0,1
48 | -0.0675,-1.88,0,0,1,0,1
49 | 0.125,-1.88,0,0,1,0,1
50 | 0.305,-1.88,0,0,1,0,1
51 | 0.525,-1.88,0,0,1,0,1
52 | 0.7625,-1.88,0,0,1,0,1
53 | 1.0075,-1.88,0,0,1,0,1
54 | 1.2775,-1.88,0,0,1,0,1
55 | 1.4925,-1.88,0,0,1,0,1
56 | 1.6825,-1.88,0,0,1,0,1
57 | 1.88,-1.69,0,-1,0,0,1
58 | 1.88,-1.4975,0,-1,0,0,1
59 | 1.88,-1.25,0,-1,0,0,1
60 | 1.88,-1.02,0,-1,0,0,1
61 | 1.88,-0.7725,0,-1,0,0,1
62 | 1.88,-0.5475,0,-1,0,0,1
63 | 1.88,-0.3025,0,-1,0,0,1
64 | 1.88,-0.0625,0,-1,0,0,1
65 |
--------------------------------------------------------------------------------
/data/arrays/wfs_university_rostock_2018.csv:
--------------------------------------------------------------------------------
1 | 1.8555,0.12942,1.6137,-1,0,0,0.1877
2 | 1.8604,0.31567,1.6137,-1,0,0,0.2045
3 | 1.8638,0.53832,1.6133,-1,0,0,0.22837
4 | 1.8665,0.77237,1.6118,-1,0,0,0.24117
5 | 1.8673,1.0206,1.6157,-1,0,0,0.24838
6 | 1.8688,1.2691,1.6154,-1,0,0,0.23781
7 | 1.8702,1.4962,1.6167,-1,0,0,0.20929
8 | 1.8755,1.6876,1.6163,-1,0,0,0.22679
9 | 1.6875,1.8702,1.6203,0,-1,0,0.22545
10 | 1.4993,1.8843,1.6154,0,-1,0,0.21679
11 | 1.2547,1.8749,1.6174,0,-1,0,0.23875
12 | 1.022,1.8768,1.6184,0,-1,0,0.23992
13 | 0.77488,1.8763,1.6175,0,-1,0,0.2349
14 | 0.55221,1.8775,1.6177,0,-1,0,0.2327
15 | 0.3095,1.8797,1.6157,0,-1,0,0.24573
16 | 0.060789,1.882,1.6134,0,-1,0,0.21554
17 | -0.12151,1.8841,1.6101,0,-1,0,0.18685
18 | -0.31278,1.8791,1.613,0,-1,0,0.20506
19 | -0.53142,1.8855,1.6099,0,-1,0,0.22562
20 | -0.76382,1.8905,1.6061,0,-1,0,0.23945
21 | -1.0102,1.8888,1.6101,0,-1,0,0.25042
22 | -1.2646,1.8911,1.6086,0,-1,0,0.23947
23 | -1.4891,1.8936,1.607,0,-1,0,0.20807
24 | -1.6807,1.8964,1.6062,0,-1,0,0.22572
25 | -1.8625,1.7108,1.6075,1,0,0,0.22016
26 | -1.863,1.5303,1.6066,1,0,0,0.21877
27 | -1.8611,1.2733,1.6107,1,0,0,0.2448
28 | -1.8653,1.0408,1.6075,1,0,0,0.23885
29 | -1.8729,0.79578,1.6054,1,0,0,0.23437
30 | -1.8704,0.5722,1.6071,1,0,0,0.23219
31 | -1.881,0.33166,1.6053,1,0,0,0.24605
32 | -1.8783,0.080365,1.6075,1,0,0,0.21801
33 | -1.8781,-0.10434,1.6061,1,0,0,0.1852
34 | -1.8798,-0.28999,1.609,1,0,0,0.20278
35 | -1.8842,-0.50982,1.6095,1,0,0,0.22814
36 | -1.8911,-0.74608,1.6054,1,0,0,0.23945
37 | -1.8901,-0.98854,1.6102,1,0,0,0.24439
38 | -1.8928,-1.2348,1.6095,1,0,0,0.24209
39 | -1.8925,-1.4727,1.6117,1,0,0,0.21306
40 | -1.8939,-1.6609,1.6115,1,0,0,0.22209
41 | -1.7127,-1.8417,1.611,0,1,0,0.21959
42 | -1.5295,-1.8417,1.6129,0,1,0,0.21598
43 | -1.2809,-1.8485,1.6079,0,1,0,0.24212
44 | -1.0454,-1.8478,1.6094,0,1,0,0.2401
45 | -0.80072,-1.8512,1.609,0,1,0,0.23619
46 | -0.57305,-1.8524,1.6082,0,1,0,0.23437
47 | -0.33198,-1.8525,1.6074,0,1,0,0.24395
48 | -0.085164,-1.854,1.6085,0,1,0,0.21792
49 | 0.10383,-1.8571,1.6082,0,1,0,0.18649
50 | 0.28774,-1.8609,1.6061,0,1,0,0.20288
51 | 0.50951,-1.8574,1.6049,0,1,0,0.22772
52 | 0.74305,-1.8643,1.6034,0,1,0,0.23983
53 | 0.989,-1.8695,1.6036,0,1,0,0.24802
54 | 1.239,-1.8649,1.6041,0,1,0,0.24388
55 | 1.4767,-1.8678,1.6054,0,1,0,0.20977
56 | 1.6585,-1.8653,1.6059,0,1,0,0.22148
57 | 1.8436,-1.6811,1.6054,-1,0,0,0.22264
58 | 1.8563,-1.4974,1.6033,-1,0,0,0.21688
59 | 1.8468,-1.248,1.6072,-1,0,0,0.24047
60 | 1.85,-1.0167,1.6076,-1,0,0,0.23909
61 | 1.8513,-0.76986,1.6101,-1,0,0,0.23739
62 | 1.8585,-0.54207,1.6076,-1,0,0,0.23585
63 | 1.8562,-0.29831,1.6107,-1,0,0,0.24122
64 | 1.857,-0.059658,1.6121,-1,0,0,0.21387
65 |
--------------------------------------------------------------------------------
/doc/README:
--------------------------------------------------------------------------------
1 | This directory holds the documentation in reStructuredText/Sphinx format.
2 | It also contains some examples (Jupyter notebooks and Python scripts).
3 | Have a look at the online documentation for the auto-generated HTML version:
4 |
5 | https://sfs-python.readthedocs.io/
6 |
7 | If you want to generate the HTML (or LaTeX/PDF) files on your computer, have a
8 | look at https://sfs-python.readthedocs.io/en/latest/contributing.html.
9 |
--------------------------------------------------------------------------------
/doc/_static/css/title.css:
--------------------------------------------------------------------------------
1 | .wy-side-nav-search>a, .wy-side-nav-search .wy-dropdown>a {
2 | font-family: "Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif;
3 | font-size: 200%;
4 | margin-top: .222em;
5 | margin-bottom: .202em;
6 | }
7 | .wy-side-nav-search {
8 | padding: 0;
9 | }
10 | form#rtd-search-form {
11 | margin-left: .809em;
12 | margin-right: .809em;
13 | }
14 | .rtd-nav a {
15 | float: left;
16 | display: block;
17 | width: 33.3%;
18 | height: 100%;
19 | padding-top: 7px;
20 | color: white;
21 | }
22 | .rtd-nav {
23 | overflow: hidden;
24 | width: 100%;
25 | height: 35px;
26 | margin-top: 15px;
27 | }
28 | .rtd-nav a:hover {
29 | background-color: #388bbd;
30 | }
31 | .rtd-nav a.active {
32 | background-color: #388bbd;
33 | }
34 |
--------------------------------------------------------------------------------
/doc/_template/layout.html:
--------------------------------------------------------------------------------
1 | {% extends "!layout.html" %}
2 | {% block sidebartitle %}
3 |
4 | {{ project }}
5 |
6 | {% include "searchbox.html" %}
7 |
8 |
13 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/doc/api.rst:
--------------------------------------------------------------------------------
1 | API Documentation
2 | =================
3 |
4 | .. automodule:: sfs
5 |
--------------------------------------------------------------------------------
/doc/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # SFS documentation build configuration file, created by
5 | # sphinx-quickstart on Tue Nov 4 14:01:37 2014.
6 | #
7 | # This file is execfile()d with the current directory set to its
8 | # containing dir.
9 | #
10 | # Note that not all possible configuration values are present in this
11 | # autogenerated file.
12 | #
13 | # All configuration values have a default; values that are commented out
14 | # serve to show the default.
15 |
16 | import sys
17 | import os
18 | from subprocess import check_output
19 |
20 | import sphinx
21 |
22 | # If extensions (or modules to document with autodoc) are in another directory,
23 | # add these directories to sys.path here. If the directory is relative to the
24 | # documentation root, use os.path.abspath to make it absolute, like shown here.
25 | #sys.path.insert(0, os.path.abspath('..'))
26 |
27 | # -- General configuration ------------------------------------------------
28 |
29 | # If your documentation needs a minimal Sphinx version, state it here.
30 | needs_sphinx = '1.3' # for sphinx.ext.napoleon
31 |
32 | # Add any Sphinx extension module names here, as strings. They can be
33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
34 | # ones.
35 | extensions = [
36 | 'sphinx.ext.autodoc',
37 | 'sphinx.ext.autosummary',
38 | 'sphinx.ext.viewcode',
39 | 'sphinx.ext.napoleon', # support for NumPy-style docstrings
40 | 'sphinx.ext.intersphinx',
41 | 'sphinx.ext.doctest',
42 | 'sphinxcontrib.bibtex',
43 | 'sphinx.ext.extlinks',
44 | 'matplotlib.sphinxext.plot_directive',
45 | 'nbsphinx',
46 | ]
47 |
48 | bibtex_bibfiles = ['references.bib']
49 |
50 | nbsphinx_execute_arguments = [
51 | "--InlineBackend.figure_formats={'svg', 'pdf'}",
52 | "--InlineBackend.rc={'figure.dpi': 96}",
53 | ]
54 |
55 | # Tell autodoc that the documentation is being generated
56 | sphinx.SFS_DOCS_ARE_BEING_BUILT = True
57 |
58 | autoclass_content = 'init'
59 | autodoc_member_order = 'bysource'
60 | autodoc_default_options = {
61 | 'members': True,
62 | 'undoc-members': True,
63 | }
64 |
65 | autosummary_generate = ['api']
66 |
67 | napoleon_google_docstring = False
68 | napoleon_numpy_docstring = True
69 | napoleon_include_private_with_doc = False
70 | napoleon_include_special_with_doc = False
71 | napoleon_use_admonition_for_examples = False
72 | napoleon_use_admonition_for_notes = False
73 | napoleon_use_admonition_for_references = False
74 | napoleon_use_ivar = False
75 | napoleon_use_param = False
76 | napoleon_use_rtype = False
77 |
78 | intersphinx_mapping = {
79 | 'python': ('https://docs.python.org/3/', None),
80 | 'numpy': ('https://docs.scipy.org/doc/numpy/', None),
81 | 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None),
82 | 'matplotlib': ('https://matplotlib.org/', None),
83 | }
84 |
85 | extlinks = {'sfs': ('https://sfs.readthedocs.io/en/3.2/%s',
86 | 'https://sfs.rtfd.io/')}
87 |
88 | plot_include_source = True
89 | plot_html_show_source_link = False
90 | plot_html_show_formats = False
91 | plot_pre_code = ''
92 | plot_rcparams = {
93 | 'savefig.bbox': 'tight',
94 | }
95 | plot_formats = ['svg', 'pdf']
96 |
97 | # use mathjax2 with
98 | # https://github.com/spatialaudio/nbsphinx/issues/572#issuecomment-853389268
99 | # and 'TeX' dictionary
100 | # in future we might switch to mathjax3 once the
101 | # 'begingroup' extension is available
102 | # http://docs.mathjax.org/en/latest/input/tex/extensions/begingroup.html#begingroup
103 | # https://mathjax.github.io/MathJax-demos-web/convert-configuration/convert-configuration.html
104 | mathjax_path = ('https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js'
105 | '?config=TeX-AMS-MML_HTMLorMML')
106 | mathjax2_config = {
107 | 'tex2jax': {
108 | 'inlineMath': [['$', '$'], ['\\(', '\\)']],
109 | 'processEscapes': True,
110 | 'ignoreClass': 'document',
111 | 'processClass': 'math|output_area',
112 | },
113 | 'TeX': {
114 | 'extensions': ['newcommand.js', 'begingroup.js'], # Support for \gdef
115 | },
116 | }
117 |
118 | # Add any paths that contain templates here, relative to this directory.
119 | templates_path = ['_template']
120 |
121 | # The suffix of source filenames.
122 | source_suffix = '.rst'
123 |
124 | # The encoding of source files.
125 | #source_encoding = 'utf-8-sig'
126 |
127 | # The master toctree document.
128 | master_doc = 'index'
129 |
130 | # General information about the project.
131 | authors = 'SFS Toolbox Developers'
132 | project = 'SFS Toolbox'
133 | copyright = '2019, ' + authors
134 |
135 | # The version info for the project you're documenting, acts as replacement for
136 | # |version| and |release|, also used in various other places throughout the
137 | # built documents.
138 | #
139 | # The short X.Y version.
140 | #version = '0.0.0'
141 | # The full version, including alpha/beta/rc tags.
142 | try:
143 | release = check_output(['git', 'describe', '--tags', '--always'])
144 | release = release.decode().strip()
145 | except Exception:
146 | release = ''
147 |
148 | # The language for content autogenerated by Sphinx. Refer to documentation
149 | # for a list of supported languages.
150 | #language = None
151 |
152 | # There are two options for replacing |today|: either, you set today to some
153 | # non-false value, then it is used:
154 | #today = ''
155 | # Else, today_fmt is used as the format for a strftime call.
156 | #today_fmt = '%B %d, %Y'
157 | try:
158 | today = check_output(['git', 'show', '-s', '--format=%ad', '--date=short'])
159 | today = today.decode().strip()
160 | except Exception:
161 | today = ''
162 |
163 | # List of patterns, relative to source directory, that match files and
164 | # directories to ignore when looking for source files.
165 | exclude_patterns = ['_build', '**/.ipynb_checkpoints']
166 |
167 | # The reST default role (used for this markup: `text`) to use for all
168 | # documents.
169 | default_role = 'any'
170 |
171 | # If true, '()' will be appended to :func: etc. cross-reference text.
172 | #add_function_parentheses = True
173 |
174 | # If true, the current module name will be prepended to all description
175 | # unit titles (such as .. function::).
176 | #add_module_names = True
177 |
178 | # If true, sectionauthor and moduleauthor directives will be shown in the
179 | # output. They are ignored by default.
180 | #show_authors = False
181 |
182 | # The name of the Pygments (syntax highlighting) style to use.
183 | pygments_style = 'sphinx'
184 |
185 | # A list of ignored prefixes for module index sorting.
186 | #modindex_common_prefix = []
187 |
188 | # If true, keep warnings as "system message" paragraphs in the built documents.
189 | #keep_warnings = False
190 |
191 | jinja_define = """
192 | {% set docname = env.doc2path(env.docname, base='doc') %}
193 | {% set latex_href = ''.join([
194 | '\href{https://github.com/sfstoolbox/sfs-python/blob/',
195 | env.config.release,
196 | '/',
197 | docname | escape_latex,
198 | '}{\sphinxcode{\sphinxupquote{',
199 | docname | escape_latex,
200 | '}}}',
201 | ]) %}
202 | """
203 |
204 | nbsphinx_prolog = jinja_define + r"""
205 | .. only:: html
206 |
207 | .. role:: raw-html(raw)
208 | :format: html
209 |
210 | .. nbinfo::
211 |
212 | This page was generated from `{{ docname }}`__.
213 | Interactive online version:
214 | :raw-html:`
`
215 |
216 | __ https://github.com/sfstoolbox/sfs-python/blob/
217 | {{ env.config.release }}/{{ docname }}
218 |
219 | .. raw:: latex
220 |
221 | \nbsphinxstartnotebook{\scriptsize\noindent\strut
222 | \textcolor{gray}{The following section was generated from {{ latex_href }}
223 | \dotfill}}
224 | """
225 |
226 | nbsphinx_epilog = jinja_define + r"""
227 | .. raw:: latex
228 |
229 | \nbsphinxstopnotebook{\scriptsize\noindent\strut
230 | \textcolor{gray}{\dotfill\ {{ latex_href }} ends here.}}
231 | """
232 |
233 |
234 | # -- Options for HTML output ----------------------------------------------
235 |
236 | html_css_files = ['css/title.css']
237 |
238 | # The theme to use for HTML and HTML Help pages. See the documentation for
239 | # a list of builtin themes.
240 | html_theme = 'sphinx_rtd_theme'
241 |
242 | # Theme options are theme-specific and customize the look and feel of a theme
243 | # further. For a list of options available for each theme, see the
244 | # documentation.
245 | html_theme_options = {
246 | 'collapse_navigation': False,
247 | }
248 |
249 | # Add any paths that contain custom themes here, relative to this directory.
250 | #html_theme_path = []
251 |
252 | # The name for this set of Sphinx documents. If None, it defaults to
253 | # " v documentation".
254 | html_title = project + ", version " + release
255 |
256 | # A shorter title for the navigation bar. Default is the same as html_title.
257 | #html_short_title = None
258 |
259 | # The name of an image file (relative to this directory) to place at the top
260 | # of the sidebar.
261 | #html_logo = None
262 |
263 | # The name of an image file (within the static path) to use as favicon of the
264 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
265 | # pixels large.
266 | #html_favicon = None
267 |
268 | # Add any paths that contain custom static files (such as style sheets) here,
269 | # relative to this directory. They are copied after the builtin static files,
270 | # so a file named "default.css" will overwrite the builtin "default.css".
271 | html_static_path = ['_static']
272 |
273 | # Add any extra paths that contain custom files (such as robots.txt or
274 | # .htaccess) here, relative to this directory. These files are copied
275 | # directly to the root of the documentation.
276 | #html_extra_path = []
277 |
278 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
279 | # using the given strftime format.
280 | #html_last_updated_fmt = '%b %d, %Y'
281 |
282 | # If true, SmartyPants will be used to convert quotes and dashes to
283 | # typographically correct entities.
284 | #html_use_smartypants = True
285 |
286 | # Custom sidebar templates, maps document names to template names.
287 | #html_sidebars = {}
288 |
289 | # Additional templates that should be rendered to pages, maps page names to
290 | # template names.
291 | #html_additional_pages = {}
292 |
293 | # If false, no module index is generated.
294 | #html_domain_indices = True
295 |
296 | # If false, no index is generated.
297 | #html_use_index = True
298 |
299 | # If true, the index is split into individual pages for each letter.
300 | #html_split_index = False
301 |
302 | # If true, links to the reST sources are added to the pages.
303 | html_show_sourcelink = True
304 | html_sourcelink_suffix = ''
305 |
306 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
307 | #html_show_sphinx = True
308 |
309 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
310 | #html_show_copyright = True
311 |
312 | # If true, an OpenSearch description file will be output, and all pages will
313 | # contain a tag referring to it. The value of this option must be the
314 | # base URL from which the finished HTML is served.
315 | #html_use_opensearch = ''
316 |
317 | # This is the file name suffix for HTML files (e.g. ".xhtml").
318 | #html_file_suffix = None
319 |
320 | # Output file base name for HTML help builder.
321 | htmlhelp_basename = 'SFS'
322 |
323 | html_scaled_image_link = False
324 |
325 | # -- Options for LaTeX output ---------------------------------------------
326 |
327 | latex_elements = {
328 | 'papersize': 'a4paper',
329 | 'printindex': '',
330 | 'sphinxsetup': r"""
331 | VerbatimColor={HTML}{F5F5F5},
332 | VerbatimBorderColor={HTML}{E0E0E0},
333 | noteBorderColor={HTML}{E0E0E0},
334 | noteborder=1.5pt,
335 | warningBorderColor={HTML}{E0E0E0},
336 | warningborder=1.5pt,
337 | warningBgColor={HTML}{FBFBFB},
338 | """,
339 | 'preamble': r"""
340 | \usepackage[sc,osf]{mathpazo}
341 | \linespread{1.05} % see http://www.tug.dk/FontCatalogue/urwpalladio/
342 | \renewcommand{\sfdefault}{pplj} % Palatino instead of sans serif
343 | \IfFileExists{zlmtt.sty}{
344 | \usepackage[light,scaled=1.05]{zlmtt} % light typewriter font from lmodern
345 | }{
346 | \renewcommand{\ttdefault}{lmtt} % typewriter font from lmodern
347 | }
348 | """,
349 | }
350 |
351 | # Grouping the document tree into LaTeX files. List of tuples
352 | # (source start file, target name, title,
353 | # author, documentclass [howto, manual, or own class]).
354 | latex_documents = [('index', 'SFS.tex', project, authors, 'howto')]
355 |
356 | # The name of an image file (relative to this directory) to place at the top of
357 | # the title page.
358 | #latex_logo = None
359 |
360 | # For "manual" documents, if this is true, then toplevel headings are parts,
361 | # not chapters.
362 | #latex_use_parts = False
363 |
364 | # If true, show page references after internal links.
365 | #latex_show_pagerefs = False
366 |
367 | # If true, show URL addresses after external links.
368 | latex_show_urls = 'footnote'
369 |
370 | # Documents to append as an appendix to all manuals.
371 | #latex_appendices = []
372 |
373 | # If false, no module index is generated.
374 | latex_domain_indices = False
375 |
376 |
377 | # -- Options for manual page output ---------------------------------------
378 |
379 | # One entry per manual page. List of tuples
380 | # (source start file, name, description, authors, manual section).
381 | #man_pages = [('index', 'sfs', project, [authors], 1)]
382 |
383 | # If true, show URL addresses after external links.
384 | #man_show_urls = False
385 |
386 |
387 | # -- Options for Texinfo output -------------------------------------------
388 |
389 | # Grouping the document tree into Texinfo files. List of tuples
390 | # (source start file, target name, title, author,
391 | # dir menu entry, description, category)
392 | #texinfo_documents = [
393 | # ('index', 'SFS', project, project, 'SFS', 'Sound Field Synthesis Toolbox.',
394 | # 'Miscellaneous'),
395 | #]
396 |
397 | # Documents to append as an appendix to all manuals.
398 | #texinfo_appendices = []
399 |
400 | # If false, no module index is generated.
401 | #texinfo_domain_indices = True
402 |
403 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
404 | #texinfo_show_urls = 'footnote'
405 |
406 | # If true, do not generate a @detailmenu in the "Top" node's menu.
407 | #texinfo_no_detailmenu = False
408 |
409 |
410 | # -- Options for epub output ----------------------------------------------
411 |
412 | epub_author = authors
413 |
--------------------------------------------------------------------------------
/doc/contributing.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../CONTRIBUTING.rst
2 |
--------------------------------------------------------------------------------
/doc/example-python-scripts.rst:
--------------------------------------------------------------------------------
1 | Example Python Scripts
2 | ======================
3 |
4 | Various example scripts are located in the directory ``doc/examples/``, e.g.
5 |
6 | * :download:`examples/horizontal_plane_arrays.py`: Computes the sound fields
7 | for various techniques, virtual sources and loudspeaker array configurations
8 | * :download:`examples/animations_pulsating_sphere.py`: Creates animations of a
9 | pulsating sphere, see also `the corresponding Jupyter notebook
10 | `__
11 | * :download:`examples/soundfigures.py`: Illustrates the synthesis of sound
12 | figures with Wave Field Synthesis
13 |
--------------------------------------------------------------------------------
/doc/examples.rst:
--------------------------------------------------------------------------------
1 | Examples
2 | ========
3 |
4 | .. only:: html
5 |
6 | You can play with the Jupyter notebooks (without having to install anything)
7 | by clicking |binder logo| on the respective example page.
8 |
9 | .. |binder logo| image:: https://mybinder.org/badge_logo.svg
10 | :target: https://mybinder.org/v2/gh/sfstoolbox/sfs-python/master?
11 | filepath=doc/examples
12 |
13 | .. toctree::
14 | :maxdepth: 1
15 |
16 | examples/sound-field-synthesis
17 | examples/modal-room-acoustics
18 | examples/mirror-image-source-model
19 | examples/animations-pulsating-sphere
20 | example-python-scripts
21 |
--------------------------------------------------------------------------------
/doc/examples/animations-pulsating-sphere.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Animations of a Pulsating Sphere"
8 | ]
9 | },
10 | {
11 | "cell_type": "code",
12 | "execution_count": null,
13 | "metadata": {},
14 | "outputs": [],
15 | "source": [
16 | "import sfs\n",
17 | "import numpy as np\n",
18 | "import matplotlib.pyplot as plt\n",
19 | "from IPython.display import HTML"
20 | ]
21 | },
22 | {
23 | "cell_type": "markdown",
24 | "metadata": {},
25 | "source": [
26 | "In this example, the sound field of a pulsating sphere is visualized.\n",
27 | "Different acoustic variables, such as sound pressure,\n",
28 | "particle velocity, and particle displacement, are simulated.\n",
29 | "The first two quantities are computed with\n",
30 | "\n",
31 | "- [sfs.fd.source.pulsating_sphere()](../sfs.fd.source.rst#sfs.fd.source.pulsating_sphere) and \n",
32 | "- [sfs.fd.source.pulsating_sphere_velocity()](../sfs.fd.source.rst#sfs.fd.source.pulsating_sphere_velocity)\n",
33 | "\n",
34 | "while the last one can be obtained by using\n",
35 | "\n",
36 | "- [sfs.fd.displacement()](../sfs.fd.rst#sfs.fd.displacement)\n",
37 | "\n",
38 | "which converts the particle velocity into displacement.\n",
39 | "\n",
40 | "A couple of additional functions are implemented in\n",
41 | "\n",
42 | "- [animations_pulsating_sphere.py](animations_pulsating_sphere.py)\n",
43 | "\n",
44 | "in order to help creating animating pictures, which is fun!"
45 | ]
46 | },
47 | {
48 | "cell_type": "code",
49 | "execution_count": null,
50 | "metadata": {},
51 | "outputs": [],
52 | "source": [
53 | "import animations_pulsating_sphere as animation"
54 | ]
55 | },
56 | {
57 | "cell_type": "code",
58 | "execution_count": null,
59 | "metadata": {},
60 | "outputs": [],
61 | "source": [
62 | "# Pulsating sphere\n",
63 | "center = [0, 0, 0]\n",
64 | "radius = 0.25\n",
65 | "amplitude = 0.05\n",
66 | "f = 1000 # frequency\n",
67 | "omega = 2 * np.pi * f # angular frequency\n",
68 | "\n",
69 | "# Axis limits\n",
70 | "figsize = (6, 6)\n",
71 | "xmin, xmax = -1, 1\n",
72 | "ymin, ymax = -1, 1\n",
73 | "\n",
74 | "# Animations\n",
75 | "frames = 20 # frames per period"
76 | ]
77 | },
78 | {
79 | "cell_type": "markdown",
80 | "metadata": {},
81 | "source": [
82 | "## Particle Displacement"
83 | ]
84 | },
85 | {
86 | "cell_type": "code",
87 | "execution_count": null,
88 | "metadata": {},
89 | "outputs": [],
90 | "source": [
91 | "grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.025)\n",
92 | "ani = animation.particle_displacement(\n",
93 | " omega, center, radius, amplitude, grid, frames, figsize, c='Gray')\n",
94 | "plt.close()\n",
95 | "HTML(ani.to_jshtml())"
96 | ]
97 | },
98 | {
99 | "cell_type": "markdown",
100 | "metadata": {},
101 | "source": [
102 | "Click the arrow button to start the animation.\n",
103 | "`to_jshtml()` allows you to play with the animation,\n",
104 | "e.g. speed up/down the animation (+/- button).\n",
105 | "Try to reverse the playback by clicking the left arrow.\n",
106 | "You'll see a sound _sink_.\n",
107 | "\n",
108 | "You can also show the animation by using `to_html5_video()`.\n",
109 | "See the [documentation](https://matplotlib.org/api/_as_gen/matplotlib.animation.ArtistAnimation.html#matplotlib.animation.ArtistAnimation.to_html5_video) for more detail.\n",
110 | "\n",
111 | "Of course, different types of grid can be chosen.\n",
112 | "Below is the particle animation using the same parameters\n",
113 | "but with a [hexagonal grid](https://www.redblobgames.com/grids/hexagons/)."
114 | ]
115 | },
116 | {
117 | "cell_type": "code",
118 | "execution_count": null,
119 | "metadata": {},
120 | "outputs": [],
121 | "source": [
122 | "def hex_grid(xlim, ylim, hex_edge, align='horizontal'):\n",
123 | " if align is 'vertical':\n",
124 | " umin, umax = ylim\n",
125 | " vmin, vmax = xlim\n",
126 | " else:\n",
127 | " umin, umax = xlim\n",
128 | " vmin, vmax = ylim\n",
129 | " du = np.sqrt(3) * hex_edge\n",
130 | " dv = 1.5 * hex_edge\n",
131 | " num_u = int((umax - umin) / du)\n",
132 | " num_v = int((vmax - vmin) / dv)\n",
133 | " u, v = np.meshgrid(np.linspace(umin, umax, num_u),\n",
134 | " np.linspace(vmin, vmax, num_v))\n",
135 | " u[::2] += 0.5 * du\n",
136 | "\n",
137 | " if align is 'vertical':\n",
138 | " grid = v, u, 0\n",
139 | " elif align is 'horizontal':\n",
140 | " grid = u, v, 0\n",
141 | " return grid"
142 | ]
143 | },
144 | {
145 | "cell_type": "code",
146 | "execution_count": null,
147 | "metadata": {},
148 | "outputs": [],
149 | "source": [
150 | "grid = hex_grid([xmin, xmax], [ymin, ymax], 0.0125, 'vertical')\n",
151 | "ani = animation.particle_displacement(\n",
152 | " omega, center, radius, amplitude, grid, frames, figsize, c='Gray')\n",
153 | "plt.close()\n",
154 | "HTML(ani.to_jshtml())"
155 | ]
156 | },
157 | {
158 | "cell_type": "markdown",
159 | "metadata": {},
160 | "source": [
161 | "Another one using a random grid."
162 | ]
163 | },
164 | {
165 | "cell_type": "code",
166 | "execution_count": null,
167 | "metadata": {},
168 | "outputs": [],
169 | "source": [
170 | "grid = [np.random.uniform(xmin, xmax, 4000),\n",
171 | " np.random.uniform(ymin, ymax, 4000), 0]\n",
172 | "ani = animation.particle_displacement(\n",
173 | " omega, center, radius, amplitude, grid, frames, figsize, c='Gray')\n",
174 | "plt.close()\n",
175 | "HTML(ani.to_jshtml())"
176 | ]
177 | },
178 | {
179 | "cell_type": "markdown",
180 | "metadata": {},
181 | "source": [
182 | "Each grid has its strengths and weaknesses. Please refer to the\n",
183 | "[on-line discussion](https://github.com/sfstoolbox/sfs-python/pull/69#issuecomment-468405536)."
184 | ]
185 | },
186 | {
187 | "cell_type": "markdown",
188 | "metadata": {},
189 | "source": [
190 | "## Particle Velocity"
191 | ]
192 | },
193 | {
194 | "cell_type": "code",
195 | "execution_count": null,
196 | "metadata": {},
197 | "outputs": [],
198 | "source": [
199 | "amplitude = 1e-3\n",
200 | "grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.04)\n",
201 | "ani = animation.particle_velocity(\n",
202 | " omega, center, radius, amplitude, grid, frames, figsize)\n",
203 | "plt.close()\n",
204 | "HTML(ani.to_jshtml())"
205 | ]
206 | },
207 | {
208 | "cell_type": "markdown",
209 | "metadata": {},
210 | "source": [
211 | "Please notice that the amplitude of the pulsating motion is adjusted\n",
212 | "so that the arrows are neither too short nor too long.\n",
213 | "This kind of compromise is inevitable since\n",
214 | "\n",
215 | "$$\n",
216 | "\\text{(particle velocity)} = \\text{i} \\omega \\times (\\text{amplitude}),\n",
217 | "$$\n",
218 | "\n",
219 | "thus the absolute value of particle velocity is usually\n",
220 | "much larger than that of amplitude.\n",
221 | "It should be also kept in mind that the hole in the middle\n",
222 | "does not visualizes the exact motion of the pulsating sphere.\n",
223 | "According to the above equation, the actual amplitude should be\n",
224 | "much smaller than the arrow lengths.\n",
225 | "The changing rate of its size is also two times higher than the original frequency."
226 | ]
227 | },
228 | {
229 | "cell_type": "markdown",
230 | "metadata": {},
231 | "source": [
232 | "## Sound Pressure"
233 | ]
234 | },
235 | {
236 | "cell_type": "code",
237 | "execution_count": null,
238 | "metadata": {},
239 | "outputs": [],
240 | "source": [
241 | "amplitude = 0.05\n",
242 | "impedance_pw = sfs.default.rho0 * sfs.default.c\n",
243 | "max_pressure = omega * impedance_pw * amplitude\n",
244 | "\n",
245 | "grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.005)\n",
246 | "ani = animation.sound_pressure(\n",
247 | " omega, center, radius, amplitude, grid, frames, pulsate=True,\n",
248 | " figsize=figsize, vmin=-max_pressure, vmax=max_pressure)\n",
249 | "plt.close()\n",
250 | "HTML(ani.to_jshtml())"
251 | ]
252 | },
253 | {
254 | "cell_type": "markdown",
255 | "metadata": {},
256 | "source": [
257 | "Notice that the sound pressure exceeds\n",
258 | "the atmospheric pressure ($\\approx 10^5$ Pa), which of course makes no sense.\n",
259 | "This is due to the large amplitude (50 mm) of the pulsating motion.\n",
260 | "It was chosen to better visualize the particle movements\n",
261 | "in the earlier animations.\n",
262 | "\n",
263 | "For 1 kHz, the amplitude corresponding to a moderate sound pressure,\n",
264 | "let say 1 Pa, is in the order of micrometer.\n",
265 | "As it is very small compared to the corresponding wavelength (0.343 m),\n",
266 | "the movement of the particles and the spatial structure of the sound field\n",
267 | "cannot be observed simultaneously.\n",
268 | "Furthermore, at high frequencies, the sound pressure\n",
269 | "for a given particle displacement scales with the frequency.\n",
270 | "The smaller wavelength (higher frequency) we choose,\n",
271 | "it is more likely to end up with a prohibitively high sound pressure.\n",
272 | "\n",
273 | "In the following examples, the amplitude is set to a realistic value 1 $\\mu$m.\n",
274 | "Notice that the pulsating motion of the sphere is no more visible."
275 | ]
276 | },
277 | {
278 | "cell_type": "code",
279 | "execution_count": null,
280 | "metadata": {},
281 | "outputs": [],
282 | "source": [
283 | "amplitude = 1e-6\n",
284 | "impedance_pw = sfs.default.rho0 * sfs.default.c\n",
285 | "max_pressure = omega * impedance_pw * amplitude\n",
286 | "\n",
287 | "grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.005)\n",
288 | "ani = animation.sound_pressure(\n",
289 | " omega, center, radius, amplitude, grid, frames, pulsate=True,\n",
290 | " figsize=figsize, vmin=-max_pressure, vmax=max_pressure)\n",
291 | "plt.close()\n",
292 | "HTML(ani.to_jshtml())"
293 | ]
294 | },
295 | {
296 | "cell_type": "markdown",
297 | "metadata": {},
298 | "source": [
299 | "Let's zoom in closer to the boundary of the sphere."
300 | ]
301 | },
302 | {
303 | "cell_type": "code",
304 | "execution_count": null,
305 | "metadata": {},
306 | "outputs": [],
307 | "source": [
308 | "L = 10 * amplitude\n",
309 | "xmin_zoom, xmax_zoom = radius - L, radius + L\n",
310 | "ymin_zoom, ymax_zoom = -L, L"
311 | ]
312 | },
313 | {
314 | "cell_type": "code",
315 | "execution_count": null,
316 | "metadata": {},
317 | "outputs": [],
318 | "source": [
319 | "grid = sfs.util.xyz_grid([xmin_zoom, xmax_zoom], [ymin_zoom, ymax_zoom], 0, spacing=L / 100)\n",
320 | "ani = animation.sound_pressure(\n",
321 | " omega, center, radius, amplitude, grid, frames, pulsate=True,\n",
322 | " figsize=figsize, vmin=-max_pressure, vmax=max_pressure)\n",
323 | "plt.close()\n",
324 | "HTML(ani.to_jshtml())"
325 | ]
326 | },
327 | {
328 | "cell_type": "markdown",
329 | "metadata": {},
330 | "source": [
331 | "This shows how the vibrating motion of the sphere (left half)\n",
332 | "changes the sound pressure of the surrounding air (right half).\n",
333 | "Notice that the sound pressure increases/decreases (more red/blue)\n",
334 | "when the surface accelerates/decelerates."
335 | ]
336 | }
337 | ],
338 | "metadata": {
339 | "kernelspec": {
340 | "display_name": "Python [default]",
341 | "language": "python",
342 | "name": "python3"
343 | },
344 | "language_info": {
345 | "codemirror_mode": {
346 | "name": "ipython",
347 | "version": 3
348 | },
349 | "file_extension": ".py",
350 | "mimetype": "text/x-python",
351 | "name": "python",
352 | "nbconvert_exporter": "python",
353 | "pygments_lexer": "ipython3",
354 | "version": "3.5.6"
355 | }
356 | },
357 | "nbformat": 4,
358 | "nbformat_minor": 2
359 | }
360 |
--------------------------------------------------------------------------------
/doc/examples/animations_pulsating_sphere.py:
--------------------------------------------------------------------------------
1 | """Animations of pulsating sphere."""
2 | import sfs
3 | import numpy as np
4 | from matplotlib import pyplot as plt
5 | from matplotlib import animation
6 |
7 |
8 | def particle_displacement(omega, center, radius, amplitude, grid, frames,
9 | figsize=(8, 8), interval=80, blit=True, **kwargs):
10 | """Generate sound particle animation."""
11 | velocity = sfs.fd.source.pulsating_sphere_velocity(
12 | omega, center, radius, amplitude, grid)
13 | displacement = sfs.fd.displacement(velocity, omega)
14 | phasor = np.exp(1j * 2 * np.pi / frames)
15 |
16 | fig, ax = plt.subplots(figsize=figsize)
17 | ax.axis([grid[0].min(), grid[0].max(), grid[1].min(), grid[1].max()])
18 | scat = sfs.plot2d.particles(grid + displacement, **kwargs)
19 |
20 | def update_frame_displacement(i):
21 | position = (grid + displacement * phasor**i).apply(np.real)
22 | position = np.column_stack([position[0].flatten(),
23 | position[1].flatten()])
24 | scat.set_offsets(position)
25 | return [scat]
26 |
27 | return animation.FuncAnimation(
28 | fig, update_frame_displacement, frames,
29 | interval=interval, blit=blit)
30 |
31 |
32 | def particle_velocity(omega, center, radius, amplitude, grid, frames,
33 | figsize=(8, 8), interval=80, blit=True, **kwargs):
34 | """Generate particle velocity animation."""
35 | velocity = sfs.fd.source.pulsating_sphere_velocity(
36 | omega, center, radius, amplitude, grid)
37 | phasor = np.exp(1j * 2 * np.pi / frames)
38 |
39 | fig, ax = plt.subplots(figsize=figsize)
40 | ax.axis([grid[0].min(), grid[0].max(), grid[1].min(), grid[1].max()])
41 | quiv = sfs.plot2d.vectors(
42 | velocity, grid, clim=[-omega * amplitude, omega * amplitude],
43 | **kwargs)
44 |
45 | def update_frame_velocity(i):
46 | quiv.set_UVC(*(velocity[:2] * phasor**i).apply(np.real))
47 | return [quiv]
48 |
49 | return animation.FuncAnimation(
50 | fig, update_frame_velocity, frames, interval=interval, blit=True)
51 |
52 |
53 | def sound_pressure(omega, center, radius, amplitude, grid, frames,
54 | pulsate=False, figsize=(8, 8), interval=80, blit=True,
55 | **kwargs):
56 | """Generate sound pressure animation."""
57 | pressure = sfs.fd.source.pulsating_sphere(
58 | omega, center, radius, amplitude, grid, inside=pulsate)
59 | phasor = np.exp(1j * 2 * np.pi / frames)
60 |
61 | fig, ax = plt.subplots(figsize=figsize)
62 | im = sfs.plot2d.amplitude(np.real(pressure), grid, **kwargs)
63 | ax.axis([grid[0].min(), grid[0].max(), grid[1].min(), grid[1].max()])
64 |
65 | def update_frame_pressure(i):
66 | distance = np.linalg.norm(grid)
67 | p = pressure * phasor**i
68 | if pulsate:
69 | p[distance <= radius + amplitude * np.real(phasor**i)] = np.nan
70 | im.set_array(np.real(p))
71 | return [im]
72 |
73 | return animation.FuncAnimation(
74 | fig, update_frame_pressure, frames, interval=interval, blit=True)
75 |
76 |
77 | if __name__ == '__main__':
78 |
79 | # Pulsating sphere
80 | center = [0, 0, 0]
81 | radius = 0.25
82 | f = 750 # frequency
83 | omega = 2 * np.pi * f # angular frequency
84 |
85 | # Axis limits
86 | xmin, xmax = -1, 1
87 | ymin, ymax = -1, 1
88 |
89 | # Animations
90 | frames = 20 # frames per period
91 |
92 | # Particle displacement
93 | amplitude = 5e-2 # amplitude of the surface displacement
94 | grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.025)
95 | ani = particle_displacement(
96 | omega, center, radius, amplitude, grid, frames, c='Gray')
97 | ani.save('pulsating_sphere_displacement.gif', dpi=80, writer='imagemagick')
98 |
99 | # Particle velocity
100 | amplitude = 1e-3 # amplitude of the surface displacement
101 | grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.04)
102 | ani = particle_velocity(
103 | omega, center, radius, amplitude, grid, frames)
104 | ani.save('pulsating_sphere_velocity.gif', dpi=80, writer='imagemagick')
105 |
106 | # Sound pressure
107 | amplitude = 1e-6 # amplitude of the surface displacement
108 | impedance_pw = sfs.default.rho0 * sfs.default.c
109 | max_pressure = omega * impedance_pw * amplitude
110 | grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.005)
111 | ani = sound_pressure(
112 | omega, center, radius, amplitude, grid, frames, pulsate=True,
113 | colorbar=True, vmin=-max_pressure, vmax=max_pressure)
114 | ani.save('pulsating_sphere_pressure.gif', dpi=80, writer='imagemagick')
115 |
--------------------------------------------------------------------------------
/doc/examples/figures/circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sfstoolbox/sfs-python/043e9dd0f6aae5eeebbed8357968a2669d29fc40/doc/examples/figures/circle.png
--------------------------------------------------------------------------------
/doc/examples/figures/cross.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sfstoolbox/sfs-python/043e9dd0f6aae5eeebbed8357968a2669d29fc40/doc/examples/figures/cross.png
--------------------------------------------------------------------------------
/doc/examples/figures/rect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sfstoolbox/sfs-python/043e9dd0f6aae5eeebbed8357968a2669d29fc40/doc/examples/figures/rect.png
--------------------------------------------------------------------------------
/doc/examples/figures/tree.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sfstoolbox/sfs-python/043e9dd0f6aae5eeebbed8357968a2669d29fc40/doc/examples/figures/tree.png
--------------------------------------------------------------------------------
/doc/examples/horizontal_plane_arrays.py:
--------------------------------------------------------------------------------
1 | """
2 | Generates sound fields for various arrays and virtual source types.
3 | """
4 |
5 | import numpy as np
6 | import matplotlib.pyplot as plt
7 | import sfs
8 |
9 |
10 | dx = 0.1 # secondary source distance
11 | N = 30 # number of secondary sources
12 | f = 1000 # frequency
13 | pw_angle = 20 # traveling direction of plane wave
14 | xs = [-1.5, 0.2, 0] # position of virtual monopole
15 | tapering = sfs.tapering.tukey # tapering window
16 | talpha = 0.3 # parameter for tapering window
17 | xnorm = [1, 1, 0] # normalization point for plots
18 | grid = sfs.util.xyz_grid([-2.5, 2.5], [-1.5, 2.5], 0, spacing=0.02)
19 | acenter = [0.3, 0.7, 0] # center and normal vector of array
20 | anormal = sfs.util.direction_vector(np.radians(35), np.radians(90))
21 |
22 | # angular frequency
23 | omega = 2 * np.pi * f
24 | # normal vector of plane wave
25 | npw = sfs.util.direction_vector(np.radians(pw_angle), np.radians(90))
26 |
27 |
28 | def compute_and_plot_soundfield(title):
29 | """Compute and plot synthesized sound field."""
30 | print('Computing', title)
31 |
32 | twin = tapering(selection, alpha=talpha)
33 | p = sfs.fd.synthesize(d, twin, array, secondary_source, grid=grid)
34 |
35 | plt.figure(figsize=(15, 15))
36 | plt.cla()
37 | sfs.plot2d.amplitude(p, grid, xnorm=xnorm)
38 | sfs.plot2d.loudspeakers(array.x, array.n, twin)
39 | sfs.plot2d.virtualsource(xs)
40 | sfs.plot2d.virtualsource([0, 0], npw, type='plane')
41 | plt.title(title)
42 | plt.grid()
43 | plt.savefig(title + '.png')
44 |
45 |
46 | # linear array, secondary point sources, virtual monopole
47 | array = sfs.array.linear(N, dx, center=acenter, orientation=anormal)
48 |
49 | d, selection, secondary_source = sfs.fd.wfs.point_3d(
50 | omega, array.x, array.n, xs)
51 | compute_and_plot_soundfield('linear_ps_wfs_3d_point')
52 |
53 | d, selection, secondary_source = sfs.fd.wfs.point_25d(
54 | omega, array.x, array.n, xs, xref=xnorm)
55 | compute_and_plot_soundfield('linear_ps_wfs_25d_point')
56 |
57 | d, selection, secondary_source = sfs.fd.wfs.point_2d(
58 | omega, array.x, array.n, xs)
59 | compute_and_plot_soundfield('linear_ps_wfs_2d_point')
60 |
61 | # linear array, secondary line sources, virtual line source
62 | d, selection, secondary_source = sfs.fd.wfs.line_2d(
63 | omega, array.x, array.n, xs)
64 | compute_and_plot_soundfield('linear_ls_wfs_2d_line')
65 |
66 |
67 | # linear array, secondary point sources, virtual plane wave
68 | d, selection, secondary_source = sfs.fd.wfs.plane_3d(
69 | omega, array.x, array.n, npw)
70 | compute_and_plot_soundfield('linear_ps_wfs_3d_plane')
71 |
72 | d, selection, secondary_source = sfs.fd.wfs.plane_25d(
73 | omega, array.x, array.n, npw, xref=xnorm)
74 | compute_and_plot_soundfield('linear_ps_wfs_25d_plane')
75 |
76 | d, selection, secondary_source = sfs.fd.wfs.plane_2d(
77 | omega, array.x, array.n, npw)
78 | compute_and_plot_soundfield('linear_ps_wfs_2d_plane')
79 |
80 |
81 | # non-uniform linear array, secondary point sources
82 | array = sfs.array.linear_diff(N//3 * [dx] + N//3 * [dx/2] + N//3 * [dx],
83 | center=acenter, orientation=anormal)
84 |
85 | d, selection, secondary_source = sfs.fd.wfs.point_25d(
86 | omega, array.x, array.n, xs, xref=xnorm)
87 | compute_and_plot_soundfield('linear_nested_ps_wfs_25d_point')
88 |
89 | d, selection, secondary_source = sfs.fd.wfs.plane_25d(
90 | omega, array.x, array.n, npw, xref=xnorm)
91 | compute_and_plot_soundfield('linear_nested_ps_wfs_25d_plane')
92 |
93 |
94 | # random sampled linear array, secondary point sources
95 | array = sfs.array.linear_random(N, dx/2, 1.5*dx, center=acenter,
96 | orientation=anormal)
97 |
98 | d, selection, secondary_source = sfs.fd.wfs.point_25d(
99 | omega, array.x, array.n, xs, xref=xnorm)
100 | compute_and_plot_soundfield('linear_random_ps_wfs_25d_point')
101 |
102 | d, selection, secondary_source = sfs.fd.wfs.plane_25d(
103 | omega, array.x, array.n, npw, xref=xnorm)
104 | compute_and_plot_soundfield('linear_random_ps_wfs_25d_plane')
105 |
106 |
107 | # rectangular array, secondary point sources
108 | array = sfs.array.rectangular((N, N//2), dx, center=acenter, orientation=anormal)
109 | d, selection, secondary_source = sfs.fd.wfs.point_25d(
110 | omega, array.x, array.n, xs, xref=xnorm)
111 | compute_and_plot_soundfield('rectangular_ps_wfs_25d_point')
112 |
113 | d, selection, secondary_source = sfs.fd.wfs.plane_25d(
114 | omega, array.x, array.n, npw, xref=xnorm)
115 | compute_and_plot_soundfield('rectangular_ps_wfs_25d_plane')
116 |
117 |
118 | # circular array, secondary point sources
119 | N = 60
120 | array = sfs.array.circular(N, 1, center=acenter)
121 | d, selection, secondary_source = sfs.fd.wfs.point_25d(
122 | omega, array.x, array.n, xs, xref=xnorm)
123 | compute_and_plot_soundfield('circular_ps_wfs_25d_point')
124 |
125 | d, selection, secondary_source = sfs.fd.wfs.plane_25d(
126 | omega, array.x, array.n, npw, xref=xnorm)
127 | compute_and_plot_soundfield('circular_ps_wfs_25d_plane')
128 |
129 |
130 | # circular array, secondary line sources, NFC-HOA
131 | array = sfs.array.circular(N, 1)
132 | xnorm = [0, 0, 0]
133 | talpha = 0 # switches off tapering
134 |
135 | d, selection, secondary_source = sfs.fd.nfchoa.plane_2d(
136 | omega, array.x, 1, npw)
137 | compute_and_plot_soundfield('circular_ls_nfchoa_2d_plane')
138 |
139 |
140 | # circular array, secondary point sources, NFC-HOA
141 | array = sfs.array.circular(N, 1)
142 | xnorm = [0, 0, 0]
143 | talpha = 0 # switches off tapering
144 |
145 | d, selection, secondary_source = sfs.fd.nfchoa.point_25d(
146 | omega, array.x, 1, xs)
147 | compute_and_plot_soundfield('circular_ps_nfchoa_25d_point')
148 |
149 | d, selection, secondary_source = sfs.fd.nfchoa.plane_25d(
150 | omega, array.x, 1, npw)
151 | compute_and_plot_soundfield('circular_ps_nfchoa_25d_plane')
152 |
--------------------------------------------------------------------------------
/doc/examples/ipython_kernel_config.py:
--------------------------------------------------------------------------------
1 | # This is a configuration file that's used when opening the Jupyter notebooks
2 | # in this directory.
3 | # See https://nbviewer.jupyter.org/github/mgeier/python-audio/blob/master/plotting/matplotlib-inline-defaults.ipynb
4 |
5 | c.InlineBackend.figure_formats = {'svg'}
6 | c.InlineBackend.rc = {'figure.dpi': 96}
7 |
--------------------------------------------------------------------------------
/doc/examples/mirror-image-source-model.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Mirror Image Sources and the Sound Field in a Rectangular Room"
8 | ]
9 | },
10 | {
11 | "cell_type": "code",
12 | "execution_count": null,
13 | "metadata": {},
14 | "outputs": [],
15 | "source": [
16 | "import matplotlib.pyplot as plt\n",
17 | "import numpy as np\n",
18 | "import sfs"
19 | ]
20 | },
21 | {
22 | "cell_type": "code",
23 | "execution_count": null,
24 | "metadata": {},
25 | "outputs": [],
26 | "source": [
27 | "L = 2, 2.7, 3 # room dimensions\n",
28 | "x0 = 1.2, 1.7, 1.5 # source position\n",
29 | "max_order = 2 # maximum order of image sources\n",
30 | "coeffs = .8, .8, .6, .6, .7, .7 # wall reflection coefficients"
31 | ]
32 | },
33 | {
34 | "cell_type": "markdown",
35 | "metadata": {},
36 | "source": [
37 | "## 2D Mirror Image Sources"
38 | ]
39 | },
40 | {
41 | "cell_type": "code",
42 | "execution_count": null,
43 | "metadata": {},
44 | "outputs": [],
45 | "source": [
46 | "xs, wall_count = sfs.util.image_sources_for_box(x0[0:2], L[0:2], max_order)\n",
47 | "source_strength = np.prod(coeffs[0:4]**wall_count, axis=1)"
48 | ]
49 | },
50 | {
51 | "cell_type": "code",
52 | "execution_count": null,
53 | "metadata": {},
54 | "outputs": [],
55 | "source": [
56 | "from matplotlib.patches import Rectangle"
57 | ]
58 | },
59 | {
60 | "cell_type": "code",
61 | "execution_count": null,
62 | "metadata": {},
63 | "outputs": [],
64 | "source": [
65 | "fig, ax = plt.subplots()\n",
66 | "ax.scatter(*xs.T, source_strength * 20)\n",
67 | "ax.add_patch(Rectangle((0, 0), L[0], L[1], fill=False))\n",
68 | "ax.set_xlabel('x / m')\n",
69 | "ax.set_ylabel('y / m')\n",
70 | "ax.axis('equal');"
71 | ]
72 | },
73 | {
74 | "cell_type": "markdown",
75 | "metadata": {},
76 | "source": [
77 | "## Monochromatic Sound Field"
78 | ]
79 | },
80 | {
81 | "cell_type": "code",
82 | "execution_count": null,
83 | "metadata": {},
84 | "outputs": [],
85 | "source": [
86 | "omega = 2 * np.pi * 1000 # angular frequency"
87 | ]
88 | },
89 | {
90 | "cell_type": "code",
91 | "execution_count": null,
92 | "metadata": {},
93 | "outputs": [],
94 | "source": [
95 | "grid = sfs.util.xyz_grid([0, L[0]], [0, L[1]], 1.5, spacing=0.02)\n",
96 | "P = sfs.fd.source.point_image_sources(omega, x0, grid, L,\n",
97 | " max_order=max_order, coeffs=coeffs)"
98 | ]
99 | },
100 | {
101 | "cell_type": "code",
102 | "execution_count": null,
103 | "metadata": {},
104 | "outputs": [],
105 | "source": [
106 | "sfs.plot2d.amplitude(P, grid, xnorm=[L[0]/2, L[1]/2, L[2]/2]);"
107 | ]
108 | },
109 | {
110 | "cell_type": "markdown",
111 | "metadata": {},
112 | "source": [
113 | "## Spatio-temporal Impulse Response"
114 | ]
115 | },
116 | {
117 | "cell_type": "code",
118 | "execution_count": null,
119 | "metadata": {},
120 | "outputs": [],
121 | "source": [
122 | "fs = 44100 # sample rate\n",
123 | "signal = [1, 0, 0], fs"
124 | ]
125 | },
126 | {
127 | "cell_type": "code",
128 | "execution_count": null,
129 | "metadata": {},
130 | "outputs": [],
131 | "source": [
132 | "grid = sfs.util.xyz_grid([0, L[0]], [0, L[1]], 1.5, spacing=0.005)\n",
133 | "p = sfs.td.source.point_image_sources(x0, signal, 0.004, grid, L, max_order,\n",
134 | " coeffs=coeffs)"
135 | ]
136 | },
137 | {
138 | "cell_type": "code",
139 | "execution_count": null,
140 | "metadata": {},
141 | "outputs": [],
142 | "source": [
143 | "sfs.plot2d.level(p, grid)\n",
144 | "sfs.plot2d.virtualsource(x0)"
145 | ]
146 | }
147 | ],
148 | "metadata": {
149 | "kernelspec": {
150 | "display_name": "Python 3",
151 | "language": "python",
152 | "name": "python3"
153 | },
154 | "language_info": {
155 | "codemirror_mode": {
156 | "name": "ipython",
157 | "version": 3
158 | },
159 | "file_extension": ".py",
160 | "mimetype": "text/x-python",
161 | "name": "python",
162 | "nbconvert_exporter": "python",
163 | "pygments_lexer": "ipython3",
164 | "version": "3.7.2+"
165 | }
166 | },
167 | "nbformat": 4,
168 | "nbformat_minor": 2
169 | }
170 |
--------------------------------------------------------------------------------
/doc/examples/modal-room-acoustics.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Modal Room Acoustics"
8 | ]
9 | },
10 | {
11 | "cell_type": "code",
12 | "execution_count": null,
13 | "metadata": {},
14 | "outputs": [],
15 | "source": [
16 | "import numpy as np\n",
17 | "import matplotlib.pyplot as plt\n",
18 | "import sfs"
19 | ]
20 | },
21 | {
22 | "cell_type": "code",
23 | "execution_count": null,
24 | "metadata": {},
25 | "outputs": [],
26 | "source": [
27 | "%matplotlib inline"
28 | ]
29 | },
30 | {
31 | "cell_type": "code",
32 | "execution_count": null,
33 | "metadata": {},
34 | "outputs": [],
35 | "source": [
36 | "x0 = 1, 3, 1.80 # source position\n",
37 | "L = 6, 6, 3 # dimensions of room\n",
38 | "deltan = 0.01 # absorption factor of walls\n",
39 | "N = 20 # maximum order of modes"
40 | ]
41 | },
42 | {
43 | "cell_type": "markdown",
44 | "metadata": {},
45 | "source": [
46 | "You can experiment with different combinations of modes:"
47 | ]
48 | },
49 | {
50 | "cell_type": "code",
51 | "execution_count": null,
52 | "metadata": {},
53 | "outputs": [],
54 | "source": [
55 | "#N = [[1], 0, 0]"
56 | ]
57 | },
58 | {
59 | "cell_type": "markdown",
60 | "metadata": {},
61 | "source": [
62 | "## Sound Field for One Frequency"
63 | ]
64 | },
65 | {
66 | "cell_type": "code",
67 | "execution_count": null,
68 | "metadata": {},
69 | "outputs": [],
70 | "source": [
71 | "f = 500 # frequency\n",
72 | "omega = 2 * np.pi * f # angular frequency"
73 | ]
74 | },
75 | {
76 | "cell_type": "code",
77 | "execution_count": null,
78 | "metadata": {},
79 | "outputs": [],
80 | "source": [
81 | "grid = sfs.util.xyz_grid([0, L[0]], [0, L[1]], L[2] / 2, spacing=.1)"
82 | ]
83 | },
84 | {
85 | "cell_type": "code",
86 | "execution_count": null,
87 | "metadata": {},
88 | "outputs": [],
89 | "source": [
90 | "p = sfs.fd.source.point_modal(omega, x0, grid, L, N=N, deltan=deltan)"
91 | ]
92 | },
93 | {
94 | "cell_type": "markdown",
95 | "metadata": {},
96 | "source": [
97 | "For now, we apply an arbitrary scaling factor to make the plot look good\n",
98 | "\n",
99 | "TODO: proper normalization"
100 | ]
101 | },
102 | {
103 | "cell_type": "code",
104 | "execution_count": null,
105 | "metadata": {},
106 | "outputs": [],
107 | "source": [
108 | "p *= 0.05"
109 | ]
110 | },
111 | {
112 | "cell_type": "code",
113 | "execution_count": null,
114 | "metadata": {},
115 | "outputs": [],
116 | "source": [
117 | "sfs.plot2d.amplitude(p, grid);"
118 | ]
119 | },
120 | {
121 | "cell_type": "markdown",
122 | "metadata": {},
123 | "source": [
124 | "## Frequency Response at One Point"
125 | ]
126 | },
127 | {
128 | "cell_type": "code",
129 | "execution_count": null,
130 | "metadata": {},
131 | "outputs": [],
132 | "source": [
133 | "f = np.linspace(20, 200, 180) # frequency\n",
134 | "omega = 2 * np.pi * f # angular frequency\n",
135 | "\n",
136 | "receiver = 1, 1, 1.8\n",
137 | "\n",
138 | "p = [sfs.fd.source.point_modal(om, x0, receiver, L, N=N, deltan=deltan)\n",
139 | " for om in omega]\n",
140 | " \n",
141 | "plt.plot(f, sfs.util.db(p))\n",
142 | "plt.xlabel('frequency / Hz')\n",
143 | "plt.ylabel('level / dB')\n",
144 | "plt.grid()"
145 | ]
146 | }
147 | ],
148 | "metadata": {
149 | "kernelspec": {
150 | "display_name": "Python 3",
151 | "language": "python",
152 | "name": "python3"
153 | },
154 | "language_info": {
155 | "codemirror_mode": {
156 | "name": "ipython",
157 | "version": 3
158 | },
159 | "file_extension": ".py",
160 | "mimetype": "text/x-python",
161 | "name": "python",
162 | "nbconvert_exporter": "python",
163 | "pygments_lexer": "ipython3",
164 | "version": "3.6.3"
165 | }
166 | },
167 | "nbformat": 4,
168 | "nbformat_minor": 2
169 | }
170 |
--------------------------------------------------------------------------------
/doc/examples/plot_particle_density.py:
--------------------------------------------------------------------------------
1 | """ Example for particle density visualization of sound sources """
2 |
3 | import numpy as np
4 | import matplotlib.pyplot as plt
5 | import sfs
6 |
7 | # simulation parameters
8 | pw_angle = 45 # traveling direction of plane wave
9 | xs = [0, 0, 0] # source position
10 | f = 300 # frequency
11 |
12 | # angular frequency
13 | omega = 2 * np.pi * f
14 | # normal vector of plane wave
15 | npw = sfs.util.direction_vector(np.radians(pw_angle))
16 | # random grid for velocity
17 | grid = [np.random.uniform(-3, 3, 40000), np.random.uniform(-3, 3, 40000), 0]
18 |
19 |
20 | def plot_particle_displacement(title):
21 | # compute displacement
22 | X = grid + amplitude * sfs.fd.displacement(v, omega)
23 | # plot displacement
24 | plt.figure(figsize=(15, 15))
25 | plt.cla()
26 | sfs.plot2d.particles(X, facecolor='black', s=3, trim=[-3, 3, -3, 3])
27 | plt.axis('off')
28 | plt.title(title)
29 | plt.grid()
30 | plt.savefig(title + '.png')
31 |
32 |
33 | # point source
34 | v = sfs.fd.source.point_velocity(omega, xs, grid)
35 | amplitude = 1.5e6
36 | plot_particle_displacement('particle_displacement_point_source')
37 |
38 | # line source
39 | v = sfs.fd.source.line_velocity(omega, xs, grid)
40 | amplitude = 1.3e6
41 | plot_particle_displacement('particle_displacement_line_source')
42 |
43 | # plane wave
44 | v = sfs.fd.source.plane_velocity(omega, xs, npw, grid)
45 | amplitude = 1e5
46 | plot_particle_displacement('particle_displacement_plane_wave')
47 |
--------------------------------------------------------------------------------
/doc/examples/run_all.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | from pathlib import Path
3 | import subprocess
4 | import sys
5 |
6 | if __name__ != '__main__':
7 | raise ImportError(__name__ + ' is not meant be imported')
8 |
9 | self = Path(__file__)
10 | cwd = self.parent
11 |
12 | for script in cwd.glob('*.py'):
13 | if self == script:
14 | # Don't call yourself!
15 | continue
16 | if script.name == 'ipython_kernel_config.py':
17 | # This is a configuration file, not an example script
18 | continue
19 | print('Running', script, '...')
20 | args = [sys.executable, str(script.relative_to(cwd))] + sys.argv[1:]
21 | result = subprocess.run(args, cwd=str(cwd))
22 | if result.returncode:
23 | print('Error running', script, file=sys.stderr)
24 | sys.exit(result.returncode)
25 |
--------------------------------------------------------------------------------
/doc/examples/sound-field-synthesis.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Sound Field Synthesis\n",
8 | "\n",
9 | "Illustrates the usage of the SFS toolbox for the simulation of different sound field synthesis methods."
10 | ]
11 | },
12 | {
13 | "cell_type": "code",
14 | "execution_count": null,
15 | "metadata": {},
16 | "outputs": [],
17 | "source": [
18 | "import numpy as np\n",
19 | "import matplotlib.pyplot as plt \n",
20 | "import sfs"
21 | ]
22 | },
23 | {
24 | "cell_type": "code",
25 | "execution_count": null,
26 | "metadata": {},
27 | "outputs": [],
28 | "source": [
29 | "# Simulation parameters\n",
30 | "number_of_secondary_sources = 56\n",
31 | "frequency = 680 # in Hz\n",
32 | "pw_angle = 30 # traveling direction of plane wave in degree\n",
33 | "xs = [-2, -1, 0] # position of virtual point source in m\n",
34 | "\n",
35 | "grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02)\n",
36 | "omega = 2 * np.pi * frequency # angular frequency\n",
37 | "npw = sfs.util.direction_vector(np.radians(pw_angle)) # normal vector of plane wave"
38 | ]
39 | },
40 | {
41 | "cell_type": "markdown",
42 | "metadata": {},
43 | "source": [
44 | "Define a helper function for synthesize and plot the sound field from the given driving signals."
45 | ]
46 | },
47 | {
48 | "cell_type": "code",
49 | "execution_count": null,
50 | "metadata": {},
51 | "outputs": [],
52 | "source": [
53 | "def sound_field(d, selection, secondary_source, array, grid, tapering=True):\n",
54 | " if tapering:\n",
55 | " tapering_window = sfs.tapering.tukey(selection, alpha=0.3)\n",
56 | " else:\n",
57 | " tapering_window = sfs.tapering.none(selection)\n",
58 | " p = sfs.fd.synthesize(d, tapering_window, array, secondary_source, grid=grid)\n",
59 | " sfs.plot2d.amplitude(p, grid, xnorm=[0, 0, 0])\n",
60 | " sfs.plot2d.loudspeakers(array.x, array.n, tapering_window)"
61 | ]
62 | },
63 | {
64 | "cell_type": "markdown",
65 | "metadata": {},
66 | "source": [
67 | "## Circular loudspeaker arrays\n",
68 | "\n",
69 | "In the following we show different sound field synthesis methods applied to a circular loudspeaker array."
70 | ]
71 | },
72 | {
73 | "cell_type": "code",
74 | "execution_count": null,
75 | "metadata": {},
76 | "outputs": [],
77 | "source": [
78 | "radius = 1.5 # in m\n",
79 | "array = sfs.array.circular(number_of_secondary_sources, radius)"
80 | ]
81 | },
82 | {
83 | "cell_type": "markdown",
84 | "metadata": {},
85 | "source": [
86 | "### Wave Field Synthesis (WFS)\n",
87 | "\n",
88 | "#### Plane wave"
89 | ]
90 | },
91 | {
92 | "cell_type": "code",
93 | "execution_count": null,
94 | "metadata": {},
95 | "outputs": [],
96 | "source": [
97 | "d, selection, secondary_source = sfs.fd.wfs.plane_25d(omega, array.x, array.n, n=npw)\n",
98 | "sound_field(d, selection, secondary_source, array, grid)"
99 | ]
100 | },
101 | {
102 | "cell_type": "markdown",
103 | "metadata": {},
104 | "source": [
105 | "#### Point source"
106 | ]
107 | },
108 | {
109 | "cell_type": "code",
110 | "execution_count": null,
111 | "metadata": {},
112 | "outputs": [],
113 | "source": [
114 | "d, selection, secondary_source = sfs.fd.wfs.point_25d(omega, array.x, array.n, xs)\n",
115 | "sound_field(d, selection, secondary_source, array, grid)"
116 | ]
117 | },
118 | {
119 | "cell_type": "markdown",
120 | "metadata": {},
121 | "source": [
122 | "### Near-Field Compensated Higher Order Ambisonics (NFC-HOA)\n",
123 | "\n",
124 | "#### Plane wave"
125 | ]
126 | },
127 | {
128 | "cell_type": "code",
129 | "execution_count": null,
130 | "metadata": {},
131 | "outputs": [],
132 | "source": [
133 | "d, selection, secondary_source = sfs.fd.nfchoa.plane_25d(omega, array.x, radius, n=npw)\n",
134 | "sound_field(d, selection, secondary_source, array, grid, tapering=False)"
135 | ]
136 | },
137 | {
138 | "cell_type": "markdown",
139 | "metadata": {},
140 | "source": [
141 | "#### Point source"
142 | ]
143 | },
144 | {
145 | "cell_type": "code",
146 | "execution_count": null,
147 | "metadata": {},
148 | "outputs": [],
149 | "source": [
150 | "d, selection, secondary_source = sfs.fd.nfchoa.point_25d(omega, array.x, radius, xs)\n",
151 | "sound_field(d, selection, secondary_source, array, grid, tapering=False)"
152 | ]
153 | },
154 | {
155 | "cell_type": "markdown",
156 | "metadata": {},
157 | "source": [
158 | "## Linear loudspeaker array\n",
159 | "\n",
160 | "In the following we show different sound field synthesis methods applied to a linear loudspeaker array."
161 | ]
162 | },
163 | {
164 | "cell_type": "code",
165 | "execution_count": null,
166 | "metadata": {},
167 | "outputs": [],
168 | "source": [
169 | "spacing = 0.07 # in m\n",
170 | "array = sfs.array.linear(number_of_secondary_sources, spacing,\n",
171 | " center=[0, -0.5, 0], orientation=[0, 1, 0])"
172 | ]
173 | },
174 | {
175 | "cell_type": "markdown",
176 | "metadata": {},
177 | "source": [
178 | "### Wave Field Synthesis (WFS)\n",
179 | "\n",
180 | "#### Plane wave"
181 | ]
182 | },
183 | {
184 | "cell_type": "code",
185 | "execution_count": null,
186 | "metadata": {},
187 | "outputs": [],
188 | "source": [
189 | "d, selection, secondary_source = sfs.fd.wfs.plane_25d(omega, array.x, array.n, npw)\n",
190 | "sound_field(d, selection, secondary_source, array, grid)"
191 | ]
192 | },
193 | {
194 | "cell_type": "markdown",
195 | "metadata": {},
196 | "source": [
197 | "#### Point source"
198 | ]
199 | },
200 | {
201 | "cell_type": "code",
202 | "execution_count": null,
203 | "metadata": {},
204 | "outputs": [],
205 | "source": [
206 | "d, selection, secondary_source = sfs.fd.wfs.point_25d(omega, array.x, array.n, xs)\n",
207 | "sound_field(d, selection, secondary_source, array, grid)"
208 | ]
209 | }
210 | ],
211 | "metadata": {
212 | "kernelspec": {
213 | "display_name": "Python 3",
214 | "language": "python",
215 | "name": "python3"
216 | },
217 | "language_info": {
218 | "codemirror_mode": {
219 | "name": "ipython",
220 | "version": 3
221 | },
222 | "file_extension": ".py",
223 | "mimetype": "text/x-python",
224 | "name": "python",
225 | "nbconvert_exporter": "python",
226 | "pygments_lexer": "ipython3",
227 | "version": "3.5.2"
228 | }
229 | },
230 | "nbformat": 4,
231 | "nbformat_minor": 2
232 | }
233 |
--------------------------------------------------------------------------------
/doc/examples/soundfigures.py:
--------------------------------------------------------------------------------
1 | """This example illustrates the synthesis of a sound figure.
2 |
3 | The sound figure is defined by a grayscale PNG image. Various example
4 | images are located in the "figures/" directory.
5 |
6 | """
7 | import numpy as np
8 | import matplotlib.pyplot as plt
9 | from PIL import Image
10 | import sfs
11 |
12 | dx = 0.10 # secondary source distance
13 | N = 60 # number of secondary sources
14 | pw_angle = [90, 45] # traveling direction of plane wave
15 | f = 2000 # frequency
16 |
17 | # angular frequency
18 | omega = 2 * np.pi * f
19 |
20 | # normal vector of plane wave
21 | npw = sfs.util.direction_vector(*np.radians(pw_angle))
22 |
23 | # spatial grid
24 | grid = sfs.util.xyz_grid([-3, 3], [-3, 3], 0, spacing=0.02)
25 |
26 | # get secondary source positions
27 | array = sfs.array.cube(N, dx)
28 |
29 | # driving function for sound figure
30 | figure = np.array(Image.open('figures/tree.png')) # read image from file
31 | figure = np.rot90(figure) # turn 0deg to the top
32 | d, selection, secondary_source = sfs.fd.wfs.soundfigure_3d(
33 | omega, array.x, array.n, figure, npw=npw)
34 |
35 | # compute synthesized sound field
36 | p = sfs.fd.synthesize(d, selection, array, secondary_source, grid=grid)
37 |
38 | # plot and save synthesized sound field
39 | plt.figure(figsize=(10, 10))
40 | sfs.plot2d.amplitude(p, grid, xnorm=[0, -2.2, 0], cmap='BrBG', colorbar=False,
41 | vmin=-1, vmax=1)
42 | plt.title('Synthesized Sound Field')
43 | plt.savefig('soundfigure.png')
44 |
45 | # plot and save level of synthesized sound field
46 | plt.figure(figsize=(12.5, 12.5))
47 | im = sfs.plot2d.level(p, grid, xnorm=[0, -2.2, 0], vmin=-50, vmax=0,
48 | colorbar_kwargs=dict(label='dB'))
49 | plt.title('Level of Synthesized Sound Field')
50 | plt.savefig('soundfigure_level.png')
51 |
--------------------------------------------------------------------------------
/doc/examples/time_domain.py:
--------------------------------------------------------------------------------
1 | """
2 | Create some examples in the time domain.
3 |
4 | Simulate and plot impulse behavior for Wave Field Synthesis.
5 |
6 | """
7 |
8 | import numpy as np
9 | import matplotlib.pyplot as plt
10 | import sfs
11 |
12 | # simulation parameters
13 | grid = sfs.util.xyz_grid([-3, 3], [-3, 3], 0, spacing=0.01)
14 | my_cmap = 'YlOrRd'
15 | N = 56 # number of secondary sources
16 | R = 1.5 # radius of spherical/circular array
17 | array = sfs.array.circular(N, R) # get secondary source positions
18 | fs = 44100 # sampling rate
19 |
20 | # unit impulse
21 | signal = [1], fs
22 |
23 | # POINT SOURCE
24 | xs = 2, 2, 0 # position of virtual source
25 | t = 0.008
26 | # compute driving signals
27 | d_delay, d_weight, selection, secondary_source = \
28 | sfs.td.wfs.point_25d(array.x, array.n, xs)
29 | d = sfs.td.wfs.driving_signals(d_delay, d_weight, signal)
30 |
31 | # test soundfield
32 | twin = sfs.tapering.tukey(selection, alpha=0.3)
33 |
34 | p = sfs.td.synthesize(d, twin, array,
35 | secondary_source, observation_time=t, grid=grid)
36 | p = p * 100 # scale absolute amplitude
37 |
38 | plt.figure(figsize=(10, 10))
39 | sfs.plot2d.level(p, grid, cmap=my_cmap)
40 | sfs.plot2d.loudspeakers(array.x, array.n, twin)
41 | plt.grid()
42 | sfs.plot2d.virtualsource(xs)
43 | plt.title('impulse_ps_wfs_25d')
44 | plt.savefig('impulse_ps_wfs_25d.png')
45 |
46 | # PLANE WAVE
47 | pw_angle = 30 # traveling direction of plane wave
48 | npw = sfs.util.direction_vector(np.radians(pw_angle))
49 | t = -0.001
50 |
51 | # compute driving signals
52 | d_delay, d_weight, selection, secondary_source = \
53 | sfs.td.wfs.plane_25d(array.x, array.n, npw)
54 | d = sfs.td.wfs.driving_signals(d_delay, d_weight, signal)
55 |
56 | # test soundfield
57 | twin = sfs.tapering.tukey(selection, alpha=0.3)
58 | p = sfs.td.synthesize(d, twin, array,
59 | secondary_source, observation_time=t, grid=grid)
60 |
61 | plt.figure(figsize=(10, 10))
62 | sfs.plot2d.level(p, grid, cmap=my_cmap)
63 | sfs.plot2d.loudspeakers(array.x, array.n, twin)
64 | plt.grid()
65 | sfs.plot2d.virtualsource([0, 0], npw, type='plane')
66 | plt.title('impulse_pw_wfs_25d')
67 | plt.savefig('impulse_pw_wfs_25d.png')
68 |
69 | # FOCUSED SOURCE
70 | xs = np.r_[0.5, 0.5, 0] # position of virtual source
71 | xref = np.r_[0, 0, 0]
72 | nfs = sfs.util.normalize_vector(xref - xs) # main n of fsource
73 | t = 0.003 # compute driving signals
74 | d_delay, d_weight, selection, secondary_source = \
75 | sfs.td.wfs.focused_25d(array.x, array.n, xs, nfs)
76 | d = sfs.td.wfs.driving_signals(d_delay, d_weight, signal)
77 |
78 | # test soundfield
79 | twin = sfs.tapering.tukey(selection, alpha=0.3)
80 | p = sfs.td.synthesize(d, twin, array,
81 | secondary_source, observation_time=t, grid=grid)
82 | p = p * 100 # scale absolute amplitude
83 |
84 | plt.figure(figsize=(10, 10))
85 | sfs.plot2d.level(p, grid, cmap=my_cmap)
86 | sfs.plot2d.loudspeakers(array.x, array.n, twin)
87 | plt.grid()
88 | sfs.plot2d.virtualsource(xs)
89 | plt.title('impulse_fs_wfs_25d')
90 | plt.savefig('impulse_fs_wfs_25d.png')
91 |
--------------------------------------------------------------------------------
/doc/examples/time_domain_nfchoa.py:
--------------------------------------------------------------------------------
1 | """Create some examples of time-domain NFC-HOA."""
2 |
3 | import numpy as np
4 | import matplotlib.pyplot as plt
5 | import sfs
6 | from scipy.signal import unit_impulse
7 |
8 | # Parameters
9 | fs = 44100 # sampling frequency
10 | grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.005)
11 | N = 60 # number of secondary sources
12 | R = 1.5 # radius of circular array
13 | array = sfs.array.circular(N, R)
14 |
15 | # Excitation signal
16 | signal = unit_impulse(512), fs, 0
17 |
18 | # Plane wave
19 | max_order = None
20 | npw = [0, -1, 0] # propagating direction
21 | t = 0 # observation time
22 | delay, weight, sos, phaseshift, selection, secondary_source = \
23 | sfs.td.nfchoa.plane_25d(array.x, R, npw, fs, max_order)
24 | d = sfs.td.nfchoa.driving_signals_25d(
25 | delay, weight, sos, phaseshift, signal)
26 | p = sfs.td.synthesize(d, selection, array, secondary_source,
27 | observation_time=t, grid=grid)
28 |
29 | plt.figure()
30 | sfs.plot2d.level(p, grid)
31 | sfs.plot2d.loudspeakers(array.x, array.n)
32 | sfs.plot2d.virtualsource([0, 0], ns=npw, type='plane')
33 | plt.savefig('impulse_pw_nfchoa_25d.png')
34 |
35 | # Point source
36 | max_order = 100
37 | xs = [1.5, 1.5, 0] # position
38 | t = np.linalg.norm(xs) / sfs.default.c # observation time
39 | delay, weight, sos, phaseshift, selection, secondary_source = \
40 | sfs.td.nfchoa.point_25d(array.x, R, xs, fs, max_order)
41 | d = sfs.td.nfchoa.driving_signals_25d(
42 | delay, weight, sos, phaseshift, signal)
43 | p = sfs.td.synthesize(d, selection, array, secondary_source,
44 | observation_time=t, grid=grid)
45 |
46 | plt.figure()
47 | sfs.plot2d.level(p, grid)
48 | sfs.plot2d.loudspeakers(array.x, array.n)
49 | sfs.plot2d.virtualsource(xs, type='point')
50 | plt.savefig('impulse_ps_nfchoa_25d.png')
51 |
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 |
3 | ----
4 |
5 | .. toctree::
6 |
7 | installation
8 | examples
9 | api
10 | references
11 | contributing
12 | version-history
13 |
14 | .. only:: html
15 |
16 | * :ref:`genindex`
17 |
--------------------------------------------------------------------------------
/doc/installation.rst:
--------------------------------------------------------------------------------
1 | Installation
2 | ============
3 |
4 | Requirements
5 | ------------
6 |
7 | Obviously, you'll need Python_.
8 | More specifically, you'll need Python 3.
9 | NumPy_ and SciPy_ are needed for the calculations.
10 | If you want to use the provided functions for plotting sound fields, you'll need
11 | Matplotlib_.
12 | However, since all results are provided as plain NumPy_ arrays, you should also
13 | be able to use any plotting library of your choice to visualize the sound
14 | fields.
15 |
16 | Instead of installing all of the requirements separately, you should probably
17 | get a Python distribution that already includes everything, e.g. Anaconda_.
18 |
19 | .. _Python: https://www.python.org/
20 | .. _NumPy: http://www.numpy.org/
21 | .. _SciPy: https://www.scipy.org/scipylib/
22 | .. _Matplotlib: https://matplotlib.org/
23 | .. _Anaconda: https://docs.anaconda.com/anaconda/
24 |
25 | Installation
26 | ------------
27 |
28 | Once you have installed the above-mentioned dependencies, you can use pip_
29 | to download and install the latest release of the Sound Field Synthesis Toolbox
30 | with a single command::
31 |
32 | python3 -m pip install sfs --user
33 |
34 | If you want to install it system-wide for all users (assuming you have the
35 | necessary rights), you can just drop the ``--user`` option.
36 |
37 | To un-install, use::
38 |
39 | python3 -m pip uninstall sfs
40 |
41 | If you want to install the latest development version of the SFS Toolbox, have a
42 | look at :doc:`contributing`.
43 |
44 | .. _pip: https://pip.pypa.io/en/latest/installing/
45 |
--------------------------------------------------------------------------------
/doc/math-definitions.rst:
--------------------------------------------------------------------------------
1 | .. raw:: latex
2 |
3 | \marginpar{% Avoid creating empty vertical space for the math definitions
4 |
5 | .. rst-class:: hidden
6 | .. math::
7 |
8 | \gdef\dirac#1{\mathop{{}\delta}\left(#1\right)}
9 | \gdef\e#1{\operatorname{e}^{#1}}
10 | \gdef\Hankel#1#2#3{\mathop{{}H_{#2}^{(#1)}}\!\left(#3\right)}
11 | \gdef\hankel#1#2#3{\mathop{{}h_{#2}^{(#1)}}\!\left(#3\right)}
12 | \gdef\i{\mathrm{i}}
13 | \gdef\scalarprod#1#2{\left\langle#1,#2\right\rangle}
14 | \gdef\vec#1{\mathbf{#1}}
15 | \gdef\wc{\frac{\omega}{c}}
16 | \gdef\w{\omega}
17 | \gdef\x{\vec{x}}
18 | \gdef\n{\vec{n}}
19 |
20 | .. raw:: latex
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/doc/readthedocs-environment.yml:
--------------------------------------------------------------------------------
1 | channels:
2 | - conda-forge
3 | dependencies:
4 | - python>=3
5 | - sphinx>=1.3.6
6 | - sphinx_rtd_theme
7 | - sphinxcontrib-bibtex
8 | - numpy
9 | - scipy
10 | - matplotlib>=1.5
11 | - ipykernel
12 | - pandoc
13 | - pip:
14 | - nbsphinx
15 |
--------------------------------------------------------------------------------
/doc/references.bib:
--------------------------------------------------------------------------------
1 | @book{Ahrens2012,
2 | author = {Ahrens, J.},
3 | title = {{Analytic Methods of Sound Field Synthesis}},
4 | publisher = {Springer},
5 | address = {Berlin Heidelberg},
6 | year = {2012},
7 | doi = {10.1007/978-3-642-25743-8}
8 | }
9 | @book{Moser2012,
10 | author = {Möser, M.},
11 | title = {{Technische Akustik}},
12 | publisher = {Springer},
13 | address = {Berlin Heidelberg},
14 | year = {2012},
15 | doi = {10.1007/978-3-642-30933-5}
16 | }
17 | @inproceedings{Spors2010,
18 | author = {Spors, S. and Ahrens, J.},
19 | title = {{Analysis and Improvement of Pre-equalization in 2.5-dimensional
20 | Wave Field Synthesis}},
21 | booktitle = {128th Convention of the Audio Engineering Society},
22 | year = {2010},
23 | url = {http://bit.ly/2Ad6RRR}
24 | }
25 | @inproceedings{Spors2009,
26 | author = {Spors, S. and Ahrens, J.},
27 | title = {{Spatial Sampling Artifacts of Wave Field Synthesis for the
28 | Reproduction of Virtual Point Sources}},
29 | booktitle = {126th Convention of the Audio Engineering Society},
30 | year = {2009},
31 | url = {http://bit.ly/2jkfboi}
32 | }
33 | @inproceedings{Spors2016,
34 | author = {Spors, S. and Schultz, F. and Rettberg, T.},
35 | title = {{Improved Driving Functions for Rectangular Loudspeaker Arrays
36 | Driven by Sound Field Synthesis}},
37 | booktitle = {42nd German Annual Conference on Acoustics (DAGA)},
38 | year = {2016},
39 | url = {http://bit.ly/2AWRo7G}
40 | }
41 | @inproceedings{Spors2008,
42 | author = {Spors, S. and Rabenstein, R. and Ahrens, J.},
43 | title = {{The Theory of Wave Field Synthesis Revisited}},
44 | booktitle = {124th Convention of the Audio Engineering Society},
45 | year = {2008},
46 | url = {http://bit.ly/2ByRjnB}
47 | }
48 | @phdthesis{Wierstorf2014,
49 | author = {Wierstorf, H.},
50 | title = {{Perceptual Assessment of Sound Field Synthesis}},
51 | school = {Technische Universität Berlin},
52 | year = {2014},
53 | doi = {10.14279/depositonce-4310}
54 | }
55 | @article{Allen1979,
56 | author = {Allen, J. B. and Berkley, D. A.},
57 | title = {{Image method for efficiently simulating small-room acoustics}},
58 | journal = {Journal of the Acoustical Society of America},
59 | volume = {65},
60 | pages = {943--950},
61 | year = {1979},
62 | doi = {10.1121/1.382599}
63 | }
64 | @article{Borish1984,
65 | author = {Borish, J.},
66 | title = {{Extension of the image model to arbitrary polyhedra}},
67 | journal = {Journal of the Acoustical Society of America},
68 | volume = {75},
69 | pages = {1827--1836},
70 | year = {1984},
71 | doi = {10.1121/1.390983}
72 | }
73 | @article{Firtha2017,
74 | author = {Gergely Firtha AND P{\'e}ter Fiala AND Frank Schultz AND
75 | Sascha Spors},
76 | title = {{Improved Referencing Schemes for 2.5D Wave Field Synthesis
77 | Driving Functions}},
78 | journal = {IEEE/ACM Trans. Audio Speech Language Process.},
79 | volume = {25},
80 | number = {5},
81 | pages = {1117-1127},
82 | year = {2017},
83 | doi = {10.1109/TASLP.2017.2689245}
84 | }
85 | @phdthesis{Start1997,
86 | author = {Evert W. Start},
87 | title = {{Direct Sound Enhancement by Wave Field Synthesis}},
88 | school = {Delft University of Technology},
89 | year = {1997}
90 | }
91 | @phdthesis{Schultz2016,
92 | author = {Frank Schultz},
93 | title = {{Sound Field Synthesis for Line Source Array Applications in
94 | Large-Scale Sound Reinforcement}},
95 | school = {University of Rostock},
96 | year = {2016},
97 | doi = {10.18453/rosdok_id00001765}
98 | }
99 |
--------------------------------------------------------------------------------
/doc/references.rst:
--------------------------------------------------------------------------------
1 | References
2 | ==========
3 |
4 | .. bibliography::
5 | :style: alpha
6 |
--------------------------------------------------------------------------------
/doc/requirements.txt:
--------------------------------------------------------------------------------
1 | Sphinx>=1.3.6
2 | Sphinx-RTD-Theme
3 | nbsphinx
4 | ipykernel
5 | sphinxcontrib-bibtex>=2.1.4
6 |
7 | NumPy
8 | SciPy
9 | matplotlib>=1.5
10 |
--------------------------------------------------------------------------------
/doc/version-history.rst:
--------------------------------------------------------------------------------
1 | .. default-role:: py:obj
2 |
3 | .. include:: ../NEWS.rst
4 |
--------------------------------------------------------------------------------
/examples:
--------------------------------------------------------------------------------
1 | doc/examples/
--------------------------------------------------------------------------------
/readthedocs.yml:
--------------------------------------------------------------------------------
1 | conda:
2 | file: doc/readthedocs-environment.yml
3 | python:
4 | pip_install: true
5 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | license_file = LICENSE
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | __version__ = "unknown"
4 |
5 | # "import" __version__
6 | for line in open("sfs/__init__.py"):
7 | if line.startswith("__version__"):
8 | exec(line)
9 | break
10 |
11 | setup(
12 | name="sfs",
13 | version=__version__,
14 | packages=find_packages(),
15 | install_requires=[
16 | 'numpy!=1.11.0', # https://github.com/sfstoolbox/sfs-python/issues/11
17 | 'scipy',
18 | ],
19 | author="SFS Toolbox Developers",
20 | author_email="sfstoolbox@gmail.com",
21 | description="Sound Field Synthesis Toolbox",
22 | long_description=open('README.rst').read(),
23 | license="MIT",
24 | keywords="audio SFS WFS Ambisonics".split(),
25 | url="http://github.com/sfstoolbox/",
26 | platforms='any',
27 | python_requires='>=3.6',
28 | classifiers=[
29 | "Development Status :: 3 - Alpha",
30 | "License :: OSI Approved :: MIT License",
31 | "Operating System :: OS Independent",
32 | "Programming Language :: Python",
33 | "Programming Language :: Python :: 3",
34 | "Programming Language :: Python :: 3.6",
35 | "Programming Language :: Python :: 3.7",
36 | "Programming Language :: Python :: 3 :: Only",
37 | "Topic :: Scientific/Engineering",
38 | ],
39 | zip_safe=True,
40 | )
41 |
--------------------------------------------------------------------------------
/sfs/__init__.py:
--------------------------------------------------------------------------------
1 | """Sound Field Synthesis Toolbox.
2 |
3 | https://sfs-python.readthedocs.io/
4 |
5 | .. rubric:: Submodules
6 |
7 | .. autosummary::
8 | :toctree:
9 |
10 | fd
11 | td
12 | array
13 | tapering
14 | plot2d
15 | plot3d
16 | util
17 |
18 | """
19 | __version__ = "0.6.2"
20 |
21 |
22 | class default:
23 | """Get/set defaults for the *sfs* module.
24 |
25 | For example, when you want to change the default speed of sound::
26 |
27 | import sfs
28 | sfs.default.c = 330
29 |
30 | """
31 |
32 | c = 343
33 | """Speed of sound."""
34 |
35 | rho0 = 1.2250
36 | """Static density of air."""
37 |
38 | selection_tolerance = 1e-6
39 | """Tolerance used for secondary source selection."""
40 |
41 | def __setattr__(self, name, value):
42 | """Only allow setting existing attributes."""
43 | if name in dir(self) and name != 'reset':
44 | super().__setattr__(name, value)
45 | else:
46 | raise AttributeError(
47 | '"default" object has no attribute ' + repr(name))
48 |
49 | def reset(self):
50 | """Reset all attributes to their "factory default"."""
51 | vars(self).clear()
52 |
53 |
54 | import sys as _sys
55 | if not getattr(_sys.modules.get('sphinx'), 'SFS_DOCS_ARE_BEING_BUILT', False):
56 | # This object shadows the 'default' class, except when the docs are built:
57 | default = default()
58 |
59 | from . import tapering
60 | from . import array
61 | from . import util
62 | try:
63 | from . import plot2d
64 | except ImportError:
65 | pass
66 | try:
67 | from . import plot3d
68 | except ImportError:
69 | pass
70 |
71 | from . import fd
72 | from . import td
73 |
--------------------------------------------------------------------------------
/sfs/fd/__init__.py:
--------------------------------------------------------------------------------
1 | """Submodules for monochromatic sound fields.
2 |
3 | .. autosummary::
4 | :toctree:
5 |
6 | source
7 |
8 | wfs
9 | nfchoa
10 | sdm
11 | esa
12 |
13 | """
14 | import numpy as _np
15 |
16 | from . import source
17 | from .. import array as _array
18 | from .. import util as _util
19 |
20 |
21 | def shiftphase(p, phase):
22 | """Shift phase of a sound field."""
23 | p = _np.asarray(p)
24 | return p * _np.exp(1j * phase)
25 |
26 |
27 | def displacement(v, omega):
28 | r"""Particle displacement.
29 |
30 | .. math::
31 |
32 | d(x, t) = \int_{-\infty}^t v(x, \tau) d\tau
33 |
34 | """
35 | return _util.as_xyz_components(v) / (1j * omega)
36 |
37 |
38 | def synthesize(d, weights, ssd, secondary_source_function, **kwargs):
39 | """Compute sound field for a generic driving function.
40 |
41 | Parameters
42 | ----------
43 | d : array_like
44 | Driving function.
45 | weights : array_like
46 | Additional weights applied during integration, e.g. source
47 | selection and tapering.
48 | ssd : sequence of between 1 and 3 array_like objects
49 | Positions, normal vectors and weights of secondary sources.
50 | A `SecondarySourceDistribution` can also be used.
51 | secondary_source_function : callable
52 | A function that generates the sound field of a secondary source.
53 | This signature is expected::
54 |
55 | secondary_source_function(
56 | position, normal_vector, **kwargs) -> numpy.ndarray
57 |
58 | **kwargs
59 | All keyword arguments are forwarded to *secondary_source_function*.
60 | This is typically used to pass the *grid* argument.
61 |
62 | """
63 | ssd = _array.as_secondary_source_distribution(ssd)
64 | if not (len(ssd.x) == len(ssd.n) == len(ssd.a) == len(d) ==
65 | len(weights)):
66 | raise ValueError("length mismatch")
67 | p = 0
68 | for x, n, a, d, weight in zip(ssd.x, ssd.n, ssd.a, d, weights):
69 | if weight != 0:
70 | p += a * weight * d * secondary_source_function(x, n, **kwargs)
71 | return p
72 |
73 |
74 | def secondary_source_point(omega, c):
75 | """Create a point source for use in `sfs.fd.synthesize()`."""
76 |
77 | def secondary_source(position, _, grid):
78 | return source.point(omega, position, grid, c=c)
79 |
80 | return secondary_source
81 |
82 |
83 | def secondary_source_line(omega, c):
84 | """Create a line source for use in `sfs.fd.synthesize()`."""
85 |
86 | def secondary_source(position, _, grid):
87 | return source.line(omega, position, grid, c=c)
88 |
89 | return secondary_source
90 |
91 |
92 | from . import esa
93 | from . import nfchoa
94 | from . import sdm
95 | from . import wfs
96 |
--------------------------------------------------------------------------------
/sfs/fd/esa.py:
--------------------------------------------------------------------------------
1 | """Compute ESA driving functions for various systems.
2 |
3 | ESA is abbreviation for equivalent scattering approach.
4 |
5 | ESA driving functions for an edge-shaped SSD are provided below.
6 | Further ESA for different geometries might be added here.
7 |
8 | Note that mode-matching (such as NFC-HOA, SDM) are equivalent
9 | to ESA in their specific geometries (spherical/circular, planar/linear).
10 |
11 | """
12 | import numpy as _np
13 | from scipy.special import jn as _jn, hankel2 as _hankel2
14 |
15 | from . import secondary_source_line as _secondary_source_line
16 | from . import secondary_source_point as _secondary_source_point
17 | from .. import util as _util
18 |
19 |
20 | def plane_2d_edge(omega, x0, n=[0, 1, 0], *, alpha=_np.pi*3/2, Nc=None,
21 | c=None):
22 | r"""Driving function for 2-dimensional plane wave with edge ESA.
23 |
24 | Driving function for a virtual plane wave using the 2-dimensional ESA
25 | for an edge-shaped secondary source distribution consisting of
26 | monopole line sources.
27 |
28 | Parameters
29 | ----------
30 | omega : float
31 | Angular frequency.
32 | x0 : int(N, 3) array_like
33 | Sequence of secondary source positions.
34 | n : (3,) array_like, optional
35 | Normal vector of synthesized plane wave.
36 | alpha : float, optional
37 | Outer angle of edge.
38 | Nc : int, optional
39 | Number of elements for series expansion of driving function. Estimated
40 | if not given.
41 | c : float, optional
42 | Speed of sound
43 |
44 | Returns
45 | -------
46 | d : (N,) numpy.ndarray
47 | Complex weights of secondary sources.
48 | selection : (N,) numpy.ndarray
49 | Boolean array containing ``True`` or ``False`` depending on
50 | whether the corresponding secondary source is "active" or not.
51 | secondary_source_function : callable
52 | A function that can be used to create the sound field of a
53 | single secondary source. See `sfs.fd.synthesize()`.
54 |
55 | Notes
56 | -----
57 | One leg of the secondary sources has to be located on the x-axis (y0=0),
58 | the edge at the origin.
59 |
60 | Derived from :cite:`Spors2016`
61 |
62 | """
63 | x0 = _np.asarray(x0)
64 | n = _util.normalize_vector(n)
65 | k = _util.wavenumber(omega, c)
66 | phi_s = _np.arctan2(n[1], n[0]) + _np.pi
67 | L = x0.shape[0]
68 |
69 | r = _np.linalg.norm(x0, axis=1)
70 | phi = _np.arctan2(x0[:, 1], x0[:, 0])
71 | phi = _np.where(phi < 0, phi + 2 * _np.pi, phi)
72 |
73 | if Nc is None:
74 | Nc = _np.ceil(2 * k * _np.max(r) * alpha / _np.pi)
75 |
76 | epsilon = _np.ones(Nc) # weights for series expansion
77 | epsilon[0] = 2
78 |
79 | d = _np.zeros(L, dtype=complex)
80 | for m in _np.arange(Nc):
81 | nu = m * _np.pi / alpha
82 | d = d + 1/epsilon[m] * _np.exp(1j*nu*_np.pi/2) * _np.sin(nu*phi_s) \
83 | * _np.cos(nu*phi) * nu/r * _jn(nu, k*r)
84 |
85 | d[phi > 0] = -d[phi > 0]
86 |
87 | selection = _util.source_selection_all(len(x0))
88 | return 4*_np.pi/alpha * d, selection, _secondary_source_line(omega, c)
89 |
90 |
91 | def plane_2d_edge_dipole_ssd(omega, x0, n=[0, 1, 0], *, alpha=_np.pi*3/2,
92 | Nc=None, c=None):
93 | r"""Driving function for 2-dimensional plane wave with edge dipole ESA.
94 |
95 | Driving function for a virtual plane wave using the 2-dimensional ESA
96 | for an edge-shaped secondary source distribution consisting of
97 | dipole line sources.
98 |
99 | Parameters
100 | ----------
101 | omega : float
102 | Angular frequency.
103 | x0 : int(N, 3) array_like
104 | Sequence of secondary source positions.
105 | n : (3,) array_like, optional
106 | Normal vector of synthesized plane wave.
107 | alpha : float, optional
108 | Outer angle of edge.
109 | Nc : int, optional
110 | Number of elements for series expansion of driving function. Estimated
111 | if not given.
112 | c : float, optional
113 | Speed of sound
114 |
115 | Returns
116 | -------
117 | d : (N,) numpy.ndarray
118 | Complex weights of secondary sources.
119 | selection : (N,) numpy.ndarray
120 | Boolean array containing ``True`` or ``False`` depending on
121 | whether the corresponding secondary source is "active" or not.
122 | secondary_source_function : callable
123 | A function that can be used to create the sound field of a
124 | single secondary source. See `sfs.fd.synthesize()`.
125 |
126 | Notes
127 | -----
128 | One leg of the secondary sources has to be located on the x-axis (y0=0),
129 | the edge at the origin.
130 |
131 | Derived from :cite:`Spors2016`
132 |
133 | """
134 | x0 = _np.asarray(x0)
135 | n = _util.normalize_vector(n)
136 | k = _util.wavenumber(omega, c)
137 | phi_s = _np.arctan2(n[1], n[0]) + _np.pi
138 | L = x0.shape[0]
139 |
140 | r = _np.linalg.norm(x0, axis=1)
141 | phi = _np.arctan2(x0[:, 1], x0[:, 0])
142 | phi = _np.where(phi < 0, phi + 2 * _np.pi, phi)
143 |
144 | if Nc is None:
145 | Nc = _np.ceil(2 * k * _np.max(r) * alpha / _np.pi)
146 |
147 | epsilon = _np.ones(Nc) # weights for series expansion
148 | epsilon[0] = 2
149 |
150 | d = _np.zeros(L, dtype=complex)
151 | for m in _np.arange(Nc):
152 | nu = m * _np.pi / alpha
153 | d = d + 1/epsilon[m] * _np.exp(1j*nu*_np.pi/2) * _np.cos(nu*phi_s) \
154 | * _np.cos(nu*phi) * _jn(nu, k*r)
155 |
156 | return 4*_np.pi/alpha * d
157 |
158 |
159 | def line_2d_edge(omega, x0, xs, *, alpha=_np.pi*3/2, Nc=None, c=None):
160 | r"""Driving function for 2-dimensional line source with edge ESA.
161 |
162 | Driving function for a virtual line source using the 2-dimensional ESA
163 | for an edge-shaped secondary source distribution consisting of line
164 | sources.
165 |
166 | Parameters
167 | ----------
168 | omega : float
169 | Angular frequency.
170 | x0 : int(N, 3) array_like
171 | Sequence of secondary source positions.
172 | xs : (3,) array_like
173 | Position of synthesized line source.
174 | alpha : float, optional
175 | Outer angle of edge.
176 | Nc : int, optional
177 | Number of elements for series expansion of driving function. Estimated
178 | if not given.
179 | c : float, optional
180 | Speed of sound
181 |
182 | Returns
183 | -------
184 | d : (N,) numpy.ndarray
185 | Complex weights of secondary sources.
186 | selection : (N,) numpy.ndarray
187 | Boolean array containing ``True`` or ``False`` depending on
188 | whether the corresponding secondary source is "active" or not.
189 | secondary_source_function : callable
190 | A function that can be used to create the sound field of a
191 | single secondary source. See `sfs.fd.synthesize()`.
192 |
193 | Notes
194 | -----
195 | One leg of the secondary sources has to be located on the x-axis (y0=0),
196 | the edge at the origin.
197 |
198 | Derived from :cite:`Spors2016`
199 |
200 | """
201 | x0 = _np.asarray(x0)
202 | k = _util.wavenumber(omega, c)
203 | phi_s = _np.arctan2(xs[1], xs[0])
204 | if phi_s < 0:
205 | phi_s = phi_s + 2 * _np.pi
206 | r_s = _np.linalg.norm(xs)
207 | L = x0.shape[0]
208 |
209 | r = _np.linalg.norm(x0, axis=1)
210 | phi = _np.arctan2(x0[:, 1], x0[:, 0])
211 | phi = _np.where(phi < 0, phi + 2 * _np.pi, phi)
212 |
213 | if Nc is None:
214 | Nc = _np.ceil(2 * k * _np.max(r) * alpha / _np.pi)
215 |
216 | epsilon = _np.ones(Nc) # weights for series expansion
217 | epsilon[0] = 2
218 |
219 | d = _np.zeros(L, dtype=complex)
220 | idx = (r <= r_s)
221 | for m in _np.arange(Nc):
222 | nu = m * _np.pi / alpha
223 | f = 1/epsilon[m] * _np.sin(nu*phi_s) * _np.cos(nu*phi) * nu/r
224 | d[idx] = d[idx] + f[idx] * _jn(nu, k*r[idx]) * _hankel2(nu, k*r_s)
225 | d[~idx] = d[~idx] + f[~idx] * _jn(nu, k*r_s) * _hankel2(nu, k*r[~idx])
226 |
227 | d[phi > 0] = -d[phi > 0]
228 |
229 | selection = _util.source_selection_all(len(x0))
230 | return -1j*_np.pi/alpha * d, selection, _secondary_source_line(omega, c)
231 |
232 |
233 | def line_2d_edge_dipole_ssd(omega, x0, xs, *, alpha=_np.pi*3/2, Nc=None,
234 | c=None):
235 | r"""Driving function for 2-dimensional line source with edge dipole ESA.
236 |
237 | Driving function for a virtual line source using the 2-dimensional ESA
238 | for an edge-shaped secondary source distribution consisting of dipole line
239 | sources.
240 |
241 | Parameters
242 | ----------
243 | omega : float
244 | Angular frequency.
245 | x0 : (N, 3) array_like
246 | Sequence of secondary source positions.
247 | xs : (3,) array_like
248 | Position of synthesized line source.
249 | alpha : float, optional
250 | Outer angle of edge.
251 | Nc : int, optional
252 | Number of elements for series expansion of driving function. Estimated
253 | if not given.
254 | c : float, optional
255 | Speed of sound
256 |
257 | Returns
258 | -------
259 | d : (N,) numpy.ndarray
260 | Complex weights of secondary sources.
261 | selection : (N,) numpy.ndarray
262 | Boolean array containing ``True`` or ``False`` depending on
263 | whether the corresponding secondary source is "active" or not.
264 | secondary_source_function : callable
265 | A function that can be used to create the sound field of a
266 | single secondary source. See `sfs.fd.synthesize()`.
267 |
268 | Notes
269 | -----
270 | One leg of the secondary sources has to be located on the x-axis (y0=0),
271 | the edge at the origin.
272 |
273 | Derived from :cite:`Spors2016`
274 |
275 | """
276 | x0 = _np.asarray(x0)
277 | k = _util.wavenumber(omega, c)
278 | phi_s = _np.arctan2(xs[1], xs[0])
279 | if phi_s < 0:
280 | phi_s = phi_s + 2 * _np.pi
281 | r_s = _np.linalg.norm(xs)
282 | L = x0.shape[0]
283 |
284 | r = _np.linalg.norm(x0, axis=1)
285 | phi = _np.arctan2(x0[:, 1], x0[:, 0])
286 | phi = _np.where(phi < 0, phi + 2 * _np.pi, phi)
287 |
288 | if Nc is None:
289 | Nc = _np.ceil(2 * k * _np.max(r) * alpha / _np.pi)
290 |
291 | epsilon = _np.ones(Nc) # weights for series expansion
292 | epsilon[0] = 2
293 |
294 | d = _np.zeros(L, dtype=complex)
295 | idx = (r <= r_s)
296 | for m in _np.arange(Nc):
297 | nu = m * _np.pi / alpha
298 | f = 1/epsilon[m] * _np.cos(nu*phi_s) * _np.cos(nu*phi)
299 | d[idx] = d[idx] + f[idx] * _jn(nu, k*r[idx]) * _hankel2(nu, k*r_s)
300 | d[~idx] = d[~idx] + f[~idx] * _jn(nu, k*r_s) * _hankel2(nu, k*r[~idx])
301 |
302 | return -1j*_np.pi/alpha * d
303 |
304 |
305 | def point_25d_edge(omega, x0, xs, *, xref=[2, -2, 0], alpha=_np.pi*3/2,
306 | Nc=None, c=None):
307 | r"""Driving function for 2.5-dimensional point source with edge ESA.
308 |
309 | Driving function for a virtual point source using the 2.5-dimensional
310 | ESA for an edge-shaped secondary source distribution consisting of point
311 | sources.
312 |
313 | Parameters
314 | ----------
315 | omega : float
316 | Angular frequency.
317 | x0 : int(N, 3) array_like
318 | Sequence of secondary source positions.
319 | xs : (3,) array_like
320 | Position of synthesized line source.
321 | xref: (3,) array_like or float
322 | Reference position or reference distance
323 | alpha : float, optional
324 | Outer angle of edge.
325 | Nc : int, optional
326 | Number of elements for series expansion of driving function. Estimated
327 | if not given.
328 | c : float, optional
329 | Speed of sound
330 |
331 | Returns
332 | -------
333 | d : (N,) numpy.ndarray
334 | Complex weights of secondary sources.
335 | selection : (N,) numpy.ndarray
336 | Boolean array containing ``True`` or ``False`` depending on
337 | whether the corresponding secondary source is "active" or not.
338 | secondary_source_function : callable
339 | A function that can be used to create the sound field of a
340 | single secondary source. See `sfs.fd.synthesize()`.
341 |
342 | Notes
343 | -----
344 | One leg of the secondary sources has to be located on the x-axis (y0=0),
345 | the edge at the origin.
346 |
347 | Derived from :cite:`Spors2016`
348 |
349 | """
350 | x0 = _np.asarray(x0)
351 | xs = _np.asarray(xs)
352 | xref = _np.asarray(xref)
353 |
354 | if _np.isscalar(xref):
355 | a = _np.linalg.norm(xref) / _np.linalg.norm(xref - xs)
356 | else:
357 | a = _np.linalg.norm(xref - x0, axis=1) / _np.linalg.norm(xref - xs)
358 |
359 | d, selection, _ = line_2d_edge(omega, x0, xs, alpha=alpha, Nc=Nc, c=c)
360 | return 1j*_np.sqrt(a) * d, selection, _secondary_source_point(omega, c)
361 |
--------------------------------------------------------------------------------
/sfs/fd/nfchoa.py:
--------------------------------------------------------------------------------
1 | """Compute NFC-HOA driving functions.
2 |
3 | .. include:: math-definitions.rst
4 |
5 | .. plot::
6 | :context: reset
7 |
8 | import matplotlib.pyplot as plt
9 | import numpy as np
10 | import sfs
11 |
12 | plt.rcParams['figure.figsize'] = 6, 6
13 |
14 | xs = -1.5, 1.5, 0
15 | # normal vector for plane wave:
16 | npw = sfs.util.direction_vector(np.radians(-45))
17 | f = 300 # Hz
18 | omega = 2 * np.pi * f
19 | R = 1.5 # Radius of circular loudspeaker array
20 |
21 | grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02)
22 |
23 | array = sfs.array.circular(N=32, R=R)
24 |
25 | def plot(d, selection, secondary_source):
26 | p = sfs.fd.synthesize(d, selection, array, secondary_source, grid=grid)
27 | sfs.plot2d.amplitude(p, grid)
28 | sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15)
29 |
30 | """
31 | import numpy as _np
32 | from scipy.special import hankel2 as _hankel2
33 |
34 | from . import secondary_source_point as _secondary_source_point
35 | from . import secondary_source_line as _secondary_source_line
36 | from .. import util as _util
37 |
38 |
39 | def plane_2d(omega, x0, r0, n=[0, 1, 0], *, max_order=None, c=None):
40 | r"""Driving function for 2-dimensional NFC-HOA for a virtual plane wave.
41 |
42 | Parameters
43 | ----------
44 | omega : float
45 | Angular frequency of plane wave.
46 | x0 : (N, 3) array_like
47 | Sequence of secondary source positions.
48 | r0 : float
49 | Radius of circular secondary source distribution.
50 | n : (3,) array_like, optional
51 | Normal vector (traveling direction) of plane wave.
52 | max_order : float, optional
53 | Maximum order of circular harmonics used for the calculation.
54 | c : float, optional
55 | Speed of sound.
56 |
57 | Returns
58 | -------
59 | d : (N,) numpy.ndarray
60 | Complex weights of secondary sources.
61 | selection : (N,) numpy.ndarray
62 | Boolean array containing only ``True`` indicating that
63 | all secondary source are "active" for NFC-HOA.
64 | secondary_source_function : callable
65 | A function that can be used to create the sound field of a
66 | single secondary source. See `sfs.fd.synthesize()`.
67 |
68 | Notes
69 | -----
70 | .. math::
71 |
72 | D(\phi_0, \omega) =
73 | -\frac{2\i}{\pi r_0}
74 | \sum_{m=-M}^M
75 | \frac{\i^{-m}}{\Hankel{2}{m}{\wc r_0}}
76 | \e{\i m (\phi_0 - \phi_\text{pw})}
77 |
78 | See :sfs:`d_nfchoa/#equation-fd-nfchoa-plane-2d`
79 |
80 | Examples
81 | --------
82 | .. plot::
83 | :context: close-figs
84 |
85 | d, selection, secondary_source = sfs.fd.nfchoa.plane_2d(
86 | omega, array.x, R, npw)
87 | plot(d, selection, secondary_source)
88 |
89 | """
90 | if max_order is None:
91 | max_order = _util.max_order_circular_harmonics(len(x0))
92 |
93 | x0 = _util.asarray_of_rows(x0)
94 | k = _util.wavenumber(omega, c)
95 | n = _util.normalize_vector(n)
96 | phi, _, r = _util.cart2sph(*n)
97 | phi0 = _util.cart2sph(*x0.T)[0]
98 | d = 0
99 | for m in range(-max_order, max_order + 1):
100 | d += 1j**-m / _hankel2(m, k * r0) * _np.exp(1j * m * (phi0 - phi))
101 | selection = _util.source_selection_all(len(x0))
102 | return -2j / (_np.pi*r0) * d, selection, _secondary_source_line(omega, c)
103 |
104 |
105 | def point_25d(omega, x0, r0, xs, *, max_order=None, c=None):
106 | r"""Driving function for 2.5-dimensional NFC-HOA for a virtual point source.
107 |
108 | Parameters
109 | ----------
110 | omega : float
111 | Angular frequency of point source.
112 | x0 : (N, 3) array_like
113 | Sequence of secondary source positions.
114 | r0 : float
115 | Radius of circular secondary source distribution.
116 | xs : (3,) array_like
117 | Position of point source.
118 | max_order : float, optional
119 | Maximum order of circular harmonics used for the calculation.
120 | c : float, optional
121 | Speed of sound.
122 |
123 | Returns
124 | -------
125 | d : (N,) numpy.ndarray
126 | Complex weights of secondary sources.
127 | selection : (N,) numpy.ndarray
128 | Boolean array containing only ``True`` indicating that
129 | all secondary source are "active" for NFC-HOA.
130 | secondary_source_function : callable
131 | A function that can be used to create the sound field of a
132 | single secondary source. See `sfs.fd.synthesize()`.
133 |
134 | Notes
135 | -----
136 | .. math::
137 |
138 | D(\phi_0, \omega) =
139 | \frac{1}{2 \pi r_0}
140 | \sum_{m=-M}^M
141 | \frac{\hankel{2}{|m|}{\wc r}}{\hankel{2}{|m|}{\wc r_0}}
142 | \e{\i m (\phi_0 - \phi)}
143 |
144 | See :sfs:`d_nfchoa/#equation-fd-nfchoa-point-25d`
145 |
146 | Examples
147 | --------
148 | .. plot::
149 | :context: close-figs
150 |
151 | d, selection, secondary_source = sfs.fd.nfchoa.point_25d(
152 | omega, array.x, R, xs)
153 | plot(d, selection, secondary_source)
154 |
155 | """
156 | if max_order is None:
157 | max_order = _util.max_order_circular_harmonics(len(x0))
158 |
159 | x0 = _util.asarray_of_rows(x0)
160 | k = _util.wavenumber(omega, c)
161 | xs = _util.asarray_1d(xs)
162 | phi, _, r = _util.cart2sph(*xs)
163 | phi0 = _util.cart2sph(*x0.T)[0]
164 | hr = _util.spherical_hn2(range(0, max_order + 1), k * r)
165 | hr0 = _util.spherical_hn2(range(0, max_order + 1), k * r0)
166 | d = 0
167 | for m in range(-max_order, max_order + 1):
168 | d += hr[abs(m)] / hr0[abs(m)] * _np.exp(1j * m * (phi0 - phi))
169 | selection = _util.source_selection_all(len(x0))
170 | return d / (2 * _np.pi * r0), selection, _secondary_source_point(omega, c)
171 |
172 |
173 | def plane_25d(omega, x0, r0, n=[0, 1, 0], *, max_order=None, c=None):
174 | r"""Driving function for 2.5-dimensional NFC-HOA for a virtual plane wave.
175 |
176 | Parameters
177 | ----------
178 | omega : float
179 | Angular frequency of point source.
180 | x0 : (N, 3) array_like
181 | Sequence of secondary source positions.
182 | r0 : float
183 | Radius of circular secondary source distribution.
184 | n : (3,) array_like, optional
185 | Normal vector (traveling direction) of plane wave.
186 | max_order : float, optional
187 | Maximum order of circular harmonics used for the calculation.
188 | c : float, optional
189 | Speed of sound.
190 |
191 | Returns
192 | -------
193 | d : (N,) numpy.ndarray
194 | Complex weights of secondary sources.
195 | selection : (N,) numpy.ndarray
196 | Boolean array containing only ``True`` indicating that
197 | all secondary source are "active" for NFC-HOA.
198 | secondary_source_function : callable
199 | A function that can be used to create the sound field of a
200 | single secondary source. See `sfs.fd.synthesize()`.
201 |
202 | Notes
203 | -----
204 | .. math::
205 |
206 | D(\phi_0, \omega) =
207 | \frac{2\i}{r_0}
208 | \sum_{m=-M}^M
209 | \frac{\i^{-|m|}}{\wc \hankel{2}{|m|}{\wc r_0}}
210 | \e{\i m (\phi_0 - \phi_\text{pw})}
211 |
212 | See :sfs:`d_nfchoa/#equation-fd-nfchoa-plane-25d`
213 |
214 | Examples
215 | --------
216 | .. plot::
217 | :context: close-figs
218 |
219 | d, selection, secondary_source = sfs.fd.nfchoa.plane_25d(
220 | omega, array.x, R, npw)
221 | plot(d, selection, secondary_source)
222 |
223 | """
224 | if max_order is None:
225 | max_order = _util.max_order_circular_harmonics(len(x0))
226 |
227 | x0 = _util.asarray_of_rows(x0)
228 | k = _util.wavenumber(omega, c)
229 | n = _util.normalize_vector(n)
230 | phi, _, r = _util.cart2sph(*n)
231 | phi0 = _util.cart2sph(*x0.T)[0]
232 | d = 0
233 | hn2 = _util.spherical_hn2(range(0, max_order + 1), k * r0)
234 | for m in range(-max_order, max_order + 1):
235 | d += (-1j)**abs(m) / (k * hn2[abs(m)]) * _np.exp(1j * m * (phi0 - phi))
236 | selection = _util.source_selection_all(len(x0))
237 | return 2*1j / r0 * d, selection, _secondary_source_point(omega, c)
238 |
--------------------------------------------------------------------------------
/sfs/fd/sdm.py:
--------------------------------------------------------------------------------
1 | """Compute SDM driving functions.
2 |
3 | .. include:: math-definitions.rst
4 |
5 | .. plot::
6 | :context: reset
7 |
8 | import matplotlib.pyplot as plt
9 | import numpy as np
10 | import sfs
11 |
12 | plt.rcParams['figure.figsize'] = 6, 6
13 |
14 | xs = -1.5, 1.5, 0
15 | # normal vector for plane wave:
16 | npw = sfs.util.direction_vector(np.radians(-45))
17 | f = 300 # Hz
18 | omega = 2 * np.pi * f
19 |
20 | grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02)
21 |
22 | array = sfs.array.linear(32, 0.2, orientation=[0, -1, 0])
23 |
24 | def plot(d, selection, secondary_source):
25 | p = sfs.fd.synthesize(d, selection, array, secondary_source, grid=grid)
26 | sfs.plot2d.amplitude(p, grid)
27 | sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15)
28 |
29 | """
30 | import numpy as _np
31 | from scipy.special import hankel2 as _hankel2
32 |
33 | from . import secondary_source_line as _secondary_source_line
34 | from . import secondary_source_point as _secondary_source_point
35 | from .. import util as _util
36 |
37 |
38 | def line_2d(omega, x0, n0, xs, *, c=None):
39 | r"""Driving function for 2-dimensional SDM for a virtual line source.
40 |
41 | Parameters
42 | ----------
43 | omega : float
44 | Angular frequency of line source.
45 | x0 : (N, 3) array_like
46 | Sequence of secondary source positions.
47 | n0 : (N, 3) array_like
48 | Sequence of normal vectors of secondary sources.
49 | xs : (3,) array_like
50 | Position of line source.
51 | c : float, optional
52 | Speed of sound.
53 |
54 | Returns
55 | -------
56 | d : (N,) numpy.ndarray
57 | Complex weights of secondary sources.
58 | selection : (N,) numpy.ndarray
59 | Boolean array containing ``True`` or ``False`` depending on
60 | whether the corresponding secondary source is "active" or not.
61 | secondary_source_function : callable
62 | A function that can be used to create the sound field of a
63 | single secondary source. See `sfs.fd.synthesize()`.
64 |
65 | Notes
66 | -----
67 | The secondary sources have to be located on the x-axis (y0=0).
68 | Derived from :cite:`Spors2009`, Eq.(9), Eq.(4).
69 |
70 | Examples
71 | --------
72 | .. plot::
73 | :context: close-figs
74 |
75 | d, selection, secondary_source = sfs.fd.sdm.line_2d(
76 | omega, array.x, array.n, xs)
77 | plot(d, selection, secondary_source)
78 |
79 | """
80 | x0 = _util.asarray_of_rows(x0)
81 | n0 = _util.asarray_of_rows(n0)
82 | xs = _util.asarray_1d(xs)
83 | k = _util.wavenumber(omega, c)
84 | ds = x0 - xs
85 | r = _np.linalg.norm(ds, axis=1)
86 | d = - 1j/2 * k * xs[1] / r * _hankel2(1, k * r)
87 | selection = _util.source_selection_all(len(x0))
88 | return d, selection, _secondary_source_line(omega, c)
89 |
90 |
91 | def plane_2d(omega, x0, n0, n=[0, 1, 0], *, c=None):
92 | r"""Driving function for 2-dimensional SDM for a virtual plane wave.
93 |
94 | Parameters
95 | ----------
96 | omega : float
97 | Angular frequency of plane wave.
98 | x0 : (N, 3) array_like
99 | Sequence of secondary source positions.
100 | n0 : (N, 3) array_like
101 | Sequence of normal vectors of secondary sources.
102 | n: (3,) array_like, optional
103 | Normal vector (traveling direction) of plane wave.
104 | c : float, optional
105 | Speed of sound.
106 |
107 | Returns
108 | -------
109 | d : (N,) numpy.ndarray
110 | Complex weights of secondary sources.
111 | selection : (N,) numpy.ndarray
112 | Boolean array containing ``True`` or ``False`` depending on
113 | whether the corresponding secondary source is "active" or not.
114 | secondary_source_function : callable
115 | A function that can be used to create the sound field of a
116 | single secondary source. See `sfs.fd.synthesize()`.
117 |
118 | Notes
119 | -----
120 | The secondary sources have to be located on the x-axis (y0=0).
121 | Derived from :cite:`Ahrens2012`, Eq.(3.73), Eq.(C.5), Eq.(C.11):
122 |
123 | .. math::
124 |
125 | D(\x_0,k) = k_\text{pw,y} \e{-\i k_\text{pw,x} x}
126 |
127 | Examples
128 | --------
129 | .. plot::
130 | :context: close-figs
131 |
132 | d, selection, secondary_source = sfs.fd.sdm.plane_2d(
133 | omega, array.x, array.n, npw)
134 | plot(d, selection, secondary_source)
135 |
136 | """
137 | x0 = _util.asarray_of_rows(x0)
138 | n0 = _util.asarray_of_rows(n0)
139 | n = _util.normalize_vector(n)
140 | k = _util.wavenumber(omega, c)
141 | d = k * n[1] * _np.exp(-1j * k * n[0] * x0[:, 0])
142 | selection = _util.source_selection_all(len(x0))
143 | return d, selection, _secondary_source_line(omega, c)
144 |
145 |
146 | def plane_25d(omega, x0, n0, n=[0, 1, 0], *, xref=[0, 0, 0], c=None):
147 | r"""Driving function for 2.5-dimensional SDM for a virtual plane wave.
148 |
149 | Parameters
150 | ----------
151 | omega : float
152 | Angular frequency of plane wave.
153 | x0 : (N, 3) array_like
154 | Sequence of secondary source positions.
155 | n0 : (N, 3) array_like
156 | Sequence of normal vectors of secondary sources.
157 | n: (3,) array_like, optional
158 | Normal vector (traveling direction) of plane wave.
159 | xref : (3,) array_like, optional
160 | Reference point for synthesized sound field.
161 | c : float, optional
162 | Speed of sound.
163 |
164 | Returns
165 | -------
166 | d : (N,) numpy.ndarray
167 | Complex weights of secondary sources.
168 | selection : (N,) numpy.ndarray
169 | Boolean array containing ``True`` or ``False`` depending on
170 | whether the corresponding secondary source is "active" or not.
171 | secondary_source_function : callable
172 | A function that can be used to create the sound field of a
173 | single secondary source. See `sfs.fd.synthesize()`.
174 |
175 | Notes
176 | -----
177 | The secondary sources have to be located on the x-axis (y0=0).
178 | Eq.(3.79) from :cite:`Ahrens2012`.
179 |
180 | Examples
181 | --------
182 | .. plot::
183 | :context: close-figs
184 |
185 | d, selection, secondary_source = sfs.fd.sdm.plane_25d(
186 | omega, array.x, array.n, npw, xref=[0, -1, 0])
187 | plot(d, selection, secondary_source)
188 |
189 | """
190 | x0 = _util.asarray_of_rows(x0)
191 | n0 = _util.asarray_of_rows(n0)
192 | n = _util.normalize_vector(n)
193 | xref = _util.asarray_1d(xref)
194 | k = _util.wavenumber(omega, c)
195 | d = 4j * _np.exp(-1j*k*n[1]*xref[1]) / _hankel2(0, k*n[1]*xref[1]) * \
196 | _np.exp(-1j*k*n[0]*x0[:, 0])
197 | selection = _util.source_selection_all(len(x0))
198 | return d, selection, _secondary_source_point(omega, c)
199 |
200 |
201 | def point_25d(omega, x0, n0, xs, *, xref=[0, 0, 0], c=None):
202 | r"""Driving function for 2.5-dimensional SDM for a virtual point source.
203 |
204 | Parameters
205 | ----------
206 | omega : float
207 | Angular frequency of point source.
208 | x0 : (N, 3) array_like
209 | Sequence of secondary source positions.
210 | n0 : (N, 3) array_like
211 | Sequence of normal vectors of secondary sources.
212 | xs: (3,) array_like
213 | Position of virtual point source.
214 | xref : (3,) array_like, optional
215 | Reference point for synthesized sound field.
216 | c : float, optional
217 | Speed of sound.
218 |
219 | Returns
220 | -------
221 | d : (N,) numpy.ndarray
222 | Complex weights of secondary sources.
223 | selection : (N,) numpy.ndarray
224 | Boolean array containing ``True`` or ``False`` depending on
225 | whether the corresponding secondary source is "active" or not.
226 | secondary_source_function : callable
227 | A function that can be used to create the sound field of a
228 | single secondary source. See `sfs.fd.synthesize()`.
229 |
230 | Notes
231 | -----
232 | The secondary sources have to be located on the x-axis (y0=0).
233 | Driving function from :cite:`Spors2010`, Eq.(24).
234 |
235 | Examples
236 | --------
237 | .. plot::
238 | :context: close-figs
239 |
240 | d, selection, secondary_source = sfs.fd.sdm.point_25d(
241 | omega, array.x, array.n, xs, xref=[0, -1, 0])
242 | plot(d, selection, secondary_source)
243 |
244 | """
245 | x0 = _util.asarray_of_rows(x0)
246 | n0 = _util.asarray_of_rows(n0)
247 | xs = _util.asarray_1d(xs)
248 | xref = _util.asarray_1d(xref)
249 | k = _util.wavenumber(omega, c)
250 | ds = x0 - xs
251 | r = _np.linalg.norm(ds, axis=1)
252 | d = 1/2 * 1j * k * _np.sqrt(xref[1] / (xref[1] - xs[1])) * \
253 | xs[1] / r * _hankel2(1, k * r)
254 | selection = _util.source_selection_all(len(x0))
255 | return d, selection, _secondary_source_point(omega, c)
256 |
--------------------------------------------------------------------------------
/sfs/plot2d.py:
--------------------------------------------------------------------------------
1 | """2D plots of sound fields etc."""
2 | import matplotlib as _mpl
3 | import matplotlib.pyplot as _plt
4 | from mpl_toolkits import axes_grid1 as _axes_grid1
5 | import numpy as _np
6 |
7 | from . import default as _default
8 | from . import util as _util
9 |
10 |
11 | def _register_cmap_clip(name, original_cmap, alpha):
12 | """Create a color map with "over" and "under" values."""
13 | from matplotlib.colors import LinearSegmentedColormap
14 | cdata = _plt.cm.datad[original_cmap]
15 | if isinstance(cdata, dict):
16 | cmap = LinearSegmentedColormap(name, cdata)
17 | else:
18 | cmap = LinearSegmentedColormap.from_list(name, cdata)
19 | cmap.set_over([alpha * c + 1 - alpha for c in cmap(1.0)[:3]])
20 | cmap.set_under([alpha * c + 1 - alpha for c in cmap(0.0)[:3]])
21 | _plt.cm.register_cmap(cmap=cmap)
22 |
23 |
24 | # The 'coolwarm' colormap is based on the paper
25 | # "Diverging Color Maps for Scientific Visualization" by Kenneth Moreland
26 | # http://www.sandia.gov/~kmorel/documents/ColorMaps/
27 | _register_cmap_clip('coolwarm_clip', 'coolwarm', 0.7)
28 |
29 |
30 | def _register_cmap_transparent(name, color):
31 | """Create a color map from a given color to transparent."""
32 | from matplotlib.colors import colorConverter, LinearSegmentedColormap
33 | red, green, blue = colorConverter.to_rgb(color)
34 | cdict = {'red': ((0, red, red), (1, red, red)),
35 | 'green': ((0, green, green), (1, green, green)),
36 | 'blue': ((0, blue, blue), (1, blue, blue)),
37 | 'alpha': ((0, 0, 0), (1, 1, 1))}
38 | cmap = LinearSegmentedColormap(name, cdict)
39 | _plt.cm.register_cmap(cmap=cmap)
40 |
41 |
42 | _register_cmap_transparent('blacktransparent', 'black')
43 |
44 |
45 | def virtualsource(xs, ns=None, type='point', *, ax=None):
46 | """Draw position/orientation of virtual source."""
47 | xs = _np.asarray(xs)
48 | ns = _np.asarray(ns)
49 | if ax is None:
50 | ax = _plt.gca()
51 |
52 | if type == 'point':
53 | vps = _plt.Circle(xs, .05, edgecolor='k', facecolor='k')
54 | ax.add_artist(vps)
55 | for n in range(1, 3):
56 | vps = _plt.Circle(xs, .05+n*0.05, edgecolor='k', fill=False)
57 | ax.add_artist(vps)
58 | elif type == 'plane':
59 | ns = 0.2 * ns
60 |
61 | ax.arrow(xs[0], xs[1], ns[0], ns[1], head_width=0.05,
62 | head_length=0.1, fc='k', ec='k')
63 |
64 |
65 | def reference(xref, *, size=0.1, ax=None):
66 | """Draw reference/normalization point."""
67 | xref = _np.asarray(xref)
68 | if ax is None:
69 | ax = _plt.gca()
70 |
71 | ax.plot((xref[0]-size, xref[0]+size), (xref[1]-size, xref[1]+size), 'k-')
72 | ax.plot((xref[0]-size, xref[0]+size), (xref[1]+size, xref[1]-size), 'k-')
73 |
74 |
75 | def secondary_sources(x0, n0, *, size=0.05, grid=None):
76 | """Simple visualization of secondary source locations.
77 |
78 | Parameters
79 | ----------
80 | x0 : (N, 3) array_like
81 | Loudspeaker positions.
82 | n0 : (N, 3) or (3,) array_like
83 | Normal vector(s) of loudspeakers.
84 | size : float, optional
85 | Size of loudspeakers in metres.
86 | grid : triple of array_like, optional
87 | If specified, only loudspeakers within the *grid* are shown.
88 | """
89 | x0 = _np.asarray(x0)
90 | n0 = _np.asarray(n0)
91 | ax = _plt.gca()
92 |
93 | # plot only secondary sources inside simulated area
94 | if grid is not None:
95 | x0, n0 = _visible_secondarysources(x0, n0, grid)
96 |
97 | # plot symbols
98 | for x00 in x0:
99 | ss = _plt.Circle(x00[0:2], size, edgecolor='k', facecolor='k')
100 | ax.add_artist(ss)
101 |
102 |
103 | def loudspeakers(x0, n0, a0=0.5, *, size=0.08, show_numbers=False, grid=None,
104 | ax=None):
105 | """Draw loudspeaker symbols at given locations and angles.
106 |
107 | Parameters
108 | ----------
109 | x0 : (N, 3) array_like
110 | Loudspeaker positions.
111 | n0 : (N, 3) or (3,) array_like
112 | Normal vector(s) of loudspeakers.
113 | a0 : float or (N,) array_like, optional
114 | Weighting factor(s) of loudspeakers.
115 | size : float, optional
116 | Size of loudspeakers in metres.
117 | show_numbers : bool, optional
118 | If ``True``, loudspeaker numbers are shown.
119 | grid : triple of array_like, optional
120 | If specified, only loudspeakers within the *grid* are shown.
121 | ax : Axes object, optional
122 | The loudspeakers are plotted into this `matplotlib.axes.Axes`
123 | object or -- if not specified -- into the current axes.
124 |
125 | """
126 | x0 = _util.asarray_of_rows(x0)
127 | n0 = _util.asarray_of_rows(n0)
128 | a0 = _util.asarray_1d(a0).reshape(-1, 1)
129 |
130 | # plot only secondary sources inside simulated area
131 | if grid is not None:
132 | x0, n0 = _visible_secondarysources(x0, n0, grid)
133 |
134 | # normalized coordinates of loudspeaker symbol (see IEC 60617-9)
135 | codes, coordinates = zip(*(
136 | (_mpl.path.Path.MOVETO, [-0.62, 0.21]),
137 | (_mpl.path.Path.LINETO, [-0.31, 0.21]),
138 | (_mpl.path.Path.LINETO, [0, 0.5]),
139 | (_mpl.path.Path.LINETO, [0, -0.5]),
140 | (_mpl.path.Path.LINETO, [-0.31, -0.21]),
141 | (_mpl.path.Path.LINETO, [-0.62, -0.21]),
142 | (_mpl.path.Path.CLOSEPOLY, [0, 0]),
143 | (_mpl.path.Path.MOVETO, [-0.31, 0.21]),
144 | (_mpl.path.Path.LINETO, [-0.31, -0.21]),
145 | ))
146 | coordinates = _np.column_stack([coordinates, _np.zeros(len(coordinates))])
147 | coordinates *= size
148 |
149 | patches = []
150 | for x00, n00 in _util.broadcast_zip(x0, n0):
151 | # rotate and translate coordinates
152 | R = _util.rotation_matrix([1, 0, 0], n00)
153 | transformed_coordinates = _np.inner(coordinates, R) + x00
154 |
155 | patches.append(_mpl.patches.PathPatch(_mpl.path.Path(
156 | transformed_coordinates[:, :2], codes)))
157 |
158 | # add collection of patches to current axis
159 | p = _mpl.collections.PatchCollection(
160 | patches, edgecolor='0', facecolor=_np.tile(1 - a0, 3))
161 | if ax is None:
162 | ax = _plt.gca()
163 | ax.add_collection(p)
164 |
165 | if show_numbers:
166 | for idx, (x00, n00) in enumerate(_util.broadcast_zip(x0, n0)):
167 | x, y = x00[:2] - 1.2 * size * n00[:2]
168 | ax.text(x, y, idx + 1, horizontalalignment='center',
169 | verticalalignment='center', clip_on=True)
170 |
171 |
172 | def _visible_secondarysources(x0, n0, grid):
173 | """Determine secondary sources which lie within *grid*."""
174 | x, y = _util.as_xyz_components(grid[:2])
175 | idx = _np.where((x0[:, 0] > x.min()) & (x0[:, 0] < x.max()) &
176 | (x0[:, 1] > y.min()) & (x0[:, 1] < x.max()))
177 | idx = _np.squeeze(idx)
178 |
179 | return x0[idx, :], n0[idx, :]
180 |
181 |
182 | def amplitude(p, grid, *, xnorm=None, cmap='coolwarm_clip',
183 | vmin=-2.0, vmax=2.0, xlabel=None, ylabel=None,
184 | colorbar=True, colorbar_kwargs={}, ax=None, **kwargs):
185 | """Two-dimensional plot of sound field (real part).
186 |
187 | Parameters
188 | ----------
189 | p : array_like
190 | Sound pressure values (or any other scalar quantity if you
191 | like). If the values are complex, the imaginary part is
192 | ignored.
193 | Typically, *p* is two-dimensional with a shape of *(Ny, Nx)*,
194 | *(Nz, Nx)* or *(Nz, Ny)*. This is the case if
195 | `sfs.util.xyz_grid()` was used with a single number for *z*,
196 | *y* or *x*, respectively.
197 | However, *p* can also be three-dimensional with a shape of *(Ny,
198 | Nx, 1)*, *(1, Nx, Nz)* or *(Ny, 1, Nz)*. This is the case if
199 | :func:`numpy.meshgrid` was used with a scalar for *z*, *y* or
200 | *x*, respectively (and of course with the default
201 | ``indexing='xy'``).
202 |
203 | .. note:: If you want to plot a single slice of a pre-computed
204 | "full" 3D sound field, make sure that the slice still
205 | has three dimensions (including one singleton
206 | dimension). This way, you can use the original *grid*
207 | of the full volume without changes.
208 | This works because the grid component corresponding to
209 | the singleton dimension is simply ignored.
210 |
211 | grid : triple or pair of numpy.ndarray
212 | The grid that was used to calculate *p*, see
213 | `sfs.util.xyz_grid()`. If *p* is two-dimensional, but
214 | *grid* has 3 components, one of them must be scalar.
215 | xnorm : array_like, optional
216 | Coordinates of a point to which the sound field should be
217 | normalized before plotting. If not specified, no normalization
218 | is used. See `sfs.util.normalize()`.
219 |
220 | Returns
221 | -------
222 | AxesImage
223 | See :func:`matplotlib.pyplot.imshow`.
224 |
225 | Other Parameters
226 | ----------------
227 | xlabel, ylabel : str
228 | Overwrite default x/y labels. Use ``xlabel=''`` and
229 | ``ylabel=''`` to remove x/y labels. The labels can be changed
230 | afterwards with :func:`matplotlib.pyplot.xlabel` and
231 | :func:`matplotlib.pyplot.ylabel`.
232 | colorbar : bool, optional
233 | If ``False``, no colorbar is created.
234 | colorbar_kwargs : dict, optional
235 | Further colorbar arguments, see `add_colorbar()`.
236 | ax : Axes, optional
237 | If given, the plot is created on *ax* instead of the current
238 | axis (see :func:`matplotlib.pyplot.gca`).
239 | cmap, vmin, vmax, **kwargs
240 | All further parameters are forwarded to
241 | :func:`matplotlib.pyplot.imshow`.
242 |
243 | See Also
244 | --------
245 | sfs.plot2d.level
246 |
247 | """
248 | p = _np.asarray(p)
249 | grid = _util.as_xyz_components(grid)
250 |
251 | # normalize sound field wrt xnorm
252 | if xnorm is not None:
253 | p = _util.normalize(p, grid, xnorm)
254 |
255 | if p.ndim == 3:
256 | if p.shape[2] == 1:
257 | p = p[:, :, 0] # first axis: y; second axis: x
258 | plotting_plane = 'xy'
259 | elif p.shape[1] == 1:
260 | p = p[:, 0, :].T # first axis: z; second axis: y
261 | plotting_plane = 'yz'
262 | elif p.shape[0] == 1:
263 | p = p[0, :, :].T # first axis: z; second axis: x
264 | plotting_plane = 'xz'
265 | else:
266 | raise ValueError("If p is 3D, one dimension must have length 1")
267 | elif len(grid) == 3:
268 | if grid[2].ndim == 0:
269 | plotting_plane = 'xy'
270 | elif grid[1].ndim == 0:
271 | plotting_plane = 'xz'
272 | elif grid[0].ndim == 0:
273 | plotting_plane = 'yz'
274 | else:
275 | raise ValueError(
276 | "If p is 2D and grid is 3D, one grid component must be scalar")
277 | else:
278 | # 2-dimensional case
279 | plotting_plane = 'xy'
280 |
281 | if plotting_plane == 'xy':
282 | x, y = grid[[0, 1]]
283 | elif plotting_plane == 'xz':
284 | x, y = grid[[0, 2]]
285 | elif plotting_plane == 'yz':
286 | x, y = grid[[1, 2]]
287 |
288 | dx = 0.5 * x.ptp() / p.shape[0]
289 | dy = 0.5 * y.ptp() / p.shape[1]
290 |
291 | if ax is None:
292 | ax = _plt.gca()
293 |
294 | # see https://github.com/matplotlib/matplotlib/issues/10567
295 | if _mpl.__version__.startswith('2.1.'):
296 | p = _np.clip(p, -1e15, 1e15) # clip to float64 range
297 |
298 | im = ax.imshow(_np.real(p), cmap=cmap, origin='lower',
299 | extent=[x.min()-dx, x.max()+dx, y.min()-dy, y.max()+dy],
300 | vmax=vmax, vmin=vmin, **kwargs)
301 | if xlabel is None:
302 | xlabel = plotting_plane[0] + ' / m'
303 | if ylabel is None:
304 | ylabel = plotting_plane[1] + ' / m'
305 | ax.set_xlabel(xlabel)
306 | ax.set_ylabel(ylabel)
307 | if colorbar:
308 | add_colorbar(im, **colorbar_kwargs)
309 | return im
310 |
311 |
312 | def level(p, grid, *, xnorm=None, power=False, cmap=None, vmax=3, vmin=-50,
313 | **kwargs):
314 | """Two-dimensional plot of level (dB) of sound field.
315 |
316 | Takes the same parameters as `sfs.plot2d.amplitude()`.
317 |
318 | Other Parameters
319 | ----------------
320 | power : bool, optional
321 | See `sfs.util.db()`.
322 |
323 | """
324 | # normalize before converting to dB!
325 | if xnorm is not None:
326 | p = _util.normalize(p, grid, xnorm)
327 | L = _util.db(p, power=power)
328 | return amplitude(L, grid=grid, xnorm=None, cmap=cmap,
329 | vmax=vmax, vmin=vmin, **kwargs)
330 |
331 |
332 | def particles(x, *, trim=None, ax=None, xlabel='x (m)', ylabel='y (m)',
333 | edgecolors=None, marker='.', s=15, **kwargs):
334 | """Plot particle positions as scatter plot.
335 |
336 | Parameters
337 | ----------
338 | x : triple or pair of array_like
339 | x, y and optionally z components of particle positions. The z
340 | components are ignored.
341 | If the values are complex, the imaginary parts are ignored.
342 |
343 | Returns
344 | -------
345 | Scatter
346 | See :func:`matplotlib.pyplot.scatter`.
347 |
348 | Other Parameters
349 | ----------------
350 | trim : array of float, optional
351 | xmin, xmax, ymin, ymax limits for which the particles are plotted.
352 | ax : Axes, optional
353 | If given, the plot is created on *ax* instead of the current
354 | axis (see :func:`matplotlib.pyplot.gca`).
355 | xlabel, ylabel : str
356 | Overwrite default x/y labels. Use ``xlabel=''`` and
357 | ``ylabel=''`` to remove x/y labels. The labels can be changed
358 | afterwards with :func:`matplotlib.pyplot.xlabel` and
359 | :func:`matplotlib.pyplot.ylabel`.
360 | edgecolors, markr, s, **kwargs
361 | All further parameters are forwarded to
362 | :func:`matplotlib.pyplot.scatter`.
363 |
364 | """
365 | XX, YY = [_np.real(c) for c in x[:2]]
366 |
367 | if trim is not None:
368 | xmin, xmax, ymin, ymax = trim
369 |
370 | idx = _np.where((XX > xmin) & (XX < xmax) & (YY > ymin) & (YY < ymax))
371 | XX = XX[idx]
372 | YY = YY[idx]
373 |
374 | if ax is None:
375 | ax = _plt.gca()
376 |
377 | if xlabel:
378 | ax.set_xlabel(xlabel)
379 | if ylabel:
380 | ax.set_ylabel(ylabel)
381 | return ax.scatter(XX, YY, edgecolors=edgecolors, marker=marker, s=s,
382 | **kwargs)
383 |
384 |
385 | def vectors(v, grid, *, cmap='blacktransparent', headlength=3,
386 | headaxislength=2.5, ax=None, clim=None, **kwargs):
387 | """Plot a vector field in the xy plane.
388 |
389 | Parameters
390 | ----------
391 | v : triple or pair of array_like
392 | x, y and optionally z components of vector field. The z
393 | components are ignored.
394 | If the values are complex, the imaginary parts are ignored.
395 | grid : triple or pair of array_like
396 | The grid that was used to calculate *v*, see
397 | `sfs.util.xyz_grid()`. Any z components are ignored.
398 |
399 | Returns
400 | -------
401 | Quiver
402 | See :func:`matplotlib.pyplot.quiver`.
403 |
404 | Other Parameters
405 | ----------------
406 | ax : Axes, optional
407 | If given, the plot is created on *ax* instead of the current
408 | axis (see :func:`matplotlib.pyplot.gca`).
409 | clim : pair of float, optional
410 | Limits for the scaling of arrow colors.
411 | See :func:`matplotlib.pyplot.quiver`.
412 | cmap, headlength, headaxislength, **kwargs
413 | All further parameters are forwarded to
414 | :func:`matplotlib.pyplot.quiver`.
415 |
416 | """
417 | v = _util.as_xyz_components(v[:2]).apply(_np.real)
418 | X, Y = _util.as_xyz_components(grid[:2])
419 | speed = _np.linalg.norm(v)
420 | with _np.errstate(invalid='ignore'):
421 | U, V = v.apply(_np.true_divide, speed)
422 | if ax is None:
423 | ax = _plt.gca()
424 | if clim is None:
425 | v_ref = 1 / (_default.rho0 * _default.c) # reference particle velocity
426 | clim = 0, 2 * v_ref
427 | return ax.quiver(X, Y, U, V, speed, cmap=cmap, pivot='mid', units='xy',
428 | angles='xy', headlength=headlength,
429 | headaxislength=headaxislength, clim=clim, **kwargs)
430 |
431 |
432 | def add_colorbar(im, *, aspect=20, pad=0.5, **kwargs):
433 | r"""Add a vertical color bar to a plot.
434 |
435 | Parameters
436 | ----------
437 | im : ScalarMappable
438 | The output of `sfs.plot2d.amplitude()`, `sfs.plot2d.level()` or any
439 | other `matplotlib.cm.ScalarMappable`.
440 | aspect : float, optional
441 | Aspect ratio of the colorbar. Strictly speaking, since the
442 | colorbar is vertical, it's actually the inverse of the aspect
443 | ratio.
444 | pad : float, optional
445 | Space between image plot and colorbar, as a fraction of the
446 | width of the colorbar.
447 |
448 | .. note:: The *pad* argument of
449 | :meth:`matplotlib.figure.Figure.colorbar` has a
450 | slightly different meaning ("fraction of original
451 | axes")!
452 | \**kwargs
453 | All further arguments are forwarded to
454 | :meth:`matplotlib.figure.Figure.colorbar`.
455 |
456 | See Also
457 | --------
458 | matplotlib.pyplot.colorbar
459 |
460 | """
461 | ax = im.axes
462 | divider = _axes_grid1.make_axes_locatable(ax)
463 | width = _axes_grid1.axes_size.AxesY(ax, aspect=1/aspect)
464 | pad = _axes_grid1.axes_size.Fraction(pad, width)
465 | current_ax = _plt.gca()
466 | cax = divider.append_axes("right", size=width, pad=pad)
467 | _plt.sca(current_ax)
468 | return ax.figure.colorbar(im, cax=cax, orientation='vertical', **kwargs)
469 |
--------------------------------------------------------------------------------
/sfs/plot3d.py:
--------------------------------------------------------------------------------
1 | """3D plots of sound fields etc."""
2 | import matplotlib.pyplot as _plt
3 |
4 |
5 | def secondary_sources(x0, n0, a0=None, *, w=0.08, h=0.08):
6 | """Plot positions and normals of a 3D secondary source distribution."""
7 | fig = _plt.figure(figsize=(15, 15))
8 | ax = fig.add_subplot(111, projection='3d')
9 | q = ax.quiver(x0[:, 0], x0[:, 1], x0[:, 2], n0[:, 0],
10 | n0[:, 1], n0[:, 2], length=0.1)
11 | _plt.xlabel('x (m)')
12 | _plt.ylabel('y (m)')
13 | _plt.title('Secondary Sources')
14 | return q
15 |
--------------------------------------------------------------------------------
/sfs/tapering.py:
--------------------------------------------------------------------------------
1 | """Weights (tapering) for the driving function.
2 |
3 | .. plot::
4 | :context: reset
5 |
6 | import sfs
7 | import matplotlib.pyplot as plt
8 | import numpy as np
9 | plt.rcParams['figure.figsize'] = 8, 3 # inch
10 | plt.rcParams['axes.grid'] = True
11 |
12 | active1 = np.zeros(101, dtype=bool)
13 | active1[5:-5] = True
14 |
15 | # The active part can wrap around from the end to the beginning:
16 | active2 = np.ones(101, dtype=bool)
17 | active2[30:-10] = False
18 |
19 | """
20 | import numpy as _np
21 |
22 |
23 | def none(active):
24 | """No tapering window.
25 |
26 | Parameters
27 | ----------
28 | active : array_like, dtype=bool
29 | A boolean array containing ``True`` for active loudspeakers.
30 |
31 | Returns
32 | -------
33 | type(active)
34 | The input, unchanged.
35 |
36 | Examples
37 | --------
38 | .. plot::
39 | :context: close-figs
40 |
41 | plt.plot(sfs.tapering.none(active1))
42 | plt.axis([-3, 103, -0.1, 1.1])
43 |
44 | .. plot::
45 | :context: close-figs
46 |
47 | plt.plot(sfs.tapering.none(active2))
48 | plt.axis([-3, 103, -0.1, 1.1])
49 |
50 | """
51 | return active
52 |
53 |
54 | def tukey(active, *, alpha):
55 | """Tukey tapering window.
56 |
57 | This uses a function similar to :func:`scipy.signal.tukey`, except
58 | that the first and last value are not zero.
59 |
60 | Parameters
61 | ----------
62 | active : array_like, dtype=bool
63 | A boolean array containing ``True`` for active loudspeakers.
64 | alpha : float
65 | Shape parameter of the Tukey window, see
66 | :func:`scipy.signal.tukey`.
67 |
68 | Returns
69 | -------
70 | (len(active),) `numpy.ndarray`
71 | Tapering weights.
72 |
73 | Examples
74 | --------
75 | .. plot::
76 | :context: close-figs
77 |
78 | plt.plot(sfs.tapering.tukey(active1, alpha=0), label='alpha = 0')
79 | plt.plot(sfs.tapering.tukey(active1, alpha=0.25), label='alpha = 0.25')
80 | plt.plot(sfs.tapering.tukey(active1, alpha=0.5), label='alpha = 0.5')
81 | plt.plot(sfs.tapering.tukey(active1, alpha=0.75), label='alpha = 0.75')
82 | plt.plot(sfs.tapering.tukey(active1, alpha=1), label='alpha = 1')
83 | plt.axis([-3, 103, -0.1, 1.1])
84 | plt.legend(loc='lower center')
85 |
86 | .. plot::
87 | :context: close-figs
88 |
89 | plt.plot(sfs.tapering.tukey(active2, alpha=0.3))
90 | plt.axis([-3, 103, -0.1, 1.1])
91 |
92 | """
93 | idx = _windowidx(active)
94 | alpha = _np.clip(alpha, 0, 1)
95 | if alpha == 0:
96 | return none(active)
97 | # design Tukey window
98 | x = _np.linspace(0, 1, len(idx) + 2)
99 | tukey = _np.ones_like(x)
100 | first_part = x < alpha / 2
101 | tukey[first_part] = 0.5 * (
102 | 1 + _np.cos(2 * _np.pi / alpha * (x[first_part] - alpha / 2)))
103 | third_part = x >= (1 - alpha / 2)
104 | tukey[third_part] = 0.5 * (
105 | 1 + _np.cos(2 * _np.pi / alpha * (x[third_part] - 1 + alpha / 2)))
106 | # fit window into tapering function
107 | result = _np.zeros(len(active))
108 | result[idx] = tukey[1:-1]
109 | return result
110 |
111 |
112 | def kaiser(active, *, beta):
113 | """Kaiser tapering window.
114 |
115 | This uses :func:`numpy.kaiser`.
116 |
117 | Parameters
118 | ----------
119 | active : array_like, dtype=bool
120 | A boolean array containing ``True`` for active loudspeakers.
121 | alpha : float
122 | Shape parameter of the Kaiser window, see :func:`numpy.kaiser`.
123 |
124 | Returns
125 | -------
126 | (len(active),) `numpy.ndarray`
127 | Tapering weights.
128 |
129 | Examples
130 | --------
131 | .. plot::
132 | :context: close-figs
133 |
134 | plt.plot(sfs.tapering.kaiser(active1, beta=0), label='beta = 0')
135 | plt.plot(sfs.tapering.kaiser(active1, beta=2), label='beta = 2')
136 | plt.plot(sfs.tapering.kaiser(active1, beta=6), label='beta = 6')
137 | plt.plot(sfs.tapering.kaiser(active1, beta=8.6), label='beta = 8.6')
138 | plt.plot(sfs.tapering.kaiser(active1, beta=14), label='beta = 14')
139 | plt.axis([-3, 103, -0.1, 1.1])
140 | plt.legend(loc='lower center')
141 |
142 | .. plot::
143 | :context: close-figs
144 |
145 | plt.plot(sfs.tapering.kaiser(active2, beta=7))
146 | plt.axis([-3, 103, -0.1, 1.1])
147 |
148 | """
149 | idx = _windowidx(active)
150 | window = _np.zeros(len(active))
151 | window[idx] = _np.kaiser(len(idx), beta)
152 | return window
153 |
154 |
155 | def _windowidx(active):
156 | """Return list of connected indices for window function.
157 |
158 | Note: Gaps within the active part are not allowed.
159 |
160 | """
161 | # find index where active loudspeakers begin (works for connected contours)
162 | if (active[0] and not active[-1]) or _np.all(active):
163 | first_idx = 0
164 | else:
165 | first_idx = _np.argmax(_np.diff(active.astype(int))) + 1
166 | # shift generic index vector to get a connected list of indices
167 | idx = _np.roll(_np.arange(len(active)), -first_idx)
168 | # remove indices of inactive secondary sources
169 | return idx[:_np.count_nonzero(active)]
170 |
--------------------------------------------------------------------------------
/sfs/td/__init__.py:
--------------------------------------------------------------------------------
1 | """Submodules for broadband sound fields.
2 |
3 | .. autosummary::
4 | :toctree:
5 |
6 | source
7 |
8 | wfs
9 | nfchoa
10 |
11 | """
12 | import numpy as _np
13 |
14 | from . import source
15 | from .. import array as _array
16 | from .. import util as _util
17 |
18 |
19 | def synthesize(signals, weights, ssd, secondary_source_function, **kwargs):
20 | """Compute sound field for an array of secondary sources.
21 |
22 | Parameters
23 | ----------
24 | signals : (N, C) array_like + float
25 | Driving signals consisting of audio data (C channels) and a
26 | sampling rate (in Hertz).
27 | A `DelayedSignal` object can also be used.
28 | weights : (C,) array_like
29 | Additional weights applied during integration, e.g. source
30 | selection and tapering.
31 | ssd : sequence of between 1 and 3 array_like objects
32 | Positions (shape ``(C, 3)``), normal vectors (shape ``(C, 3)``)
33 | and weights (shape ``(C,)``) of secondary sources.
34 | A `SecondarySourceDistribution` can also be used.
35 | secondary_source_function : callable
36 | A function that generates the sound field of a secondary source.
37 | This signature is expected::
38 |
39 | secondary_source_function(
40 | position, normal_vector, **kwargs) -> numpy.ndarray
41 |
42 | **kwargs
43 | All keyword arguments are forwarded to *secondary_source_function*.
44 | This is typically used to pass the *observation_time* and *grid*
45 | arguments.
46 |
47 | Returns
48 | -------
49 | numpy.ndarray
50 | Sound pressure at grid positions.
51 |
52 | """
53 | ssd = _array.as_secondary_source_distribution(ssd)
54 | data, samplerate, signal_offset = _util.as_delayed_signal(signals)
55 | weights = _util.asarray_1d(weights)
56 | channels = data.T
57 | if not (len(ssd.x) == len(ssd.n) == len(ssd.a) == len(channels) ==
58 | len(weights)):
59 | raise ValueError("Length mismatch")
60 | p = 0
61 | for x, n, a, channel, weight in zip(ssd.x, ssd.n, ssd.a,
62 | channels, weights):
63 | if weight != 0:
64 | signal = channel, samplerate, signal_offset
65 | p += a * weight * secondary_source_function(x, n, signal, **kwargs)
66 | return p
67 |
68 |
69 | def apply_delays(signal, delays):
70 | """Apply delays for every channel.
71 |
72 | Parameters
73 | ----------
74 | signal : (N,) array_like + float
75 | Excitation signal consisting of (mono) audio data and a sampling
76 | rate (in Hertz). A `DelayedSignal` object can also be used.
77 | delays : (C,) array_like
78 | Delay in seconds for each channel (C), negative values allowed.
79 |
80 | Returns
81 | -------
82 | `DelayedSignal`
83 | A tuple containing the delayed signals (in a `numpy.ndarray`
84 | with shape ``(N, C)``), followed by the sampling rate (in Hertz)
85 | and a (possibly negative) time offset (in seconds).
86 |
87 | """
88 | data, samplerate, initial_offset = _util.as_delayed_signal(signal)
89 | data = _util.asarray_1d(data)
90 | delays = _util.asarray_1d(delays)
91 | delays += initial_offset
92 |
93 | delays_samples = _np.rint(samplerate * delays).astype(int)
94 | offset_samples = delays_samples.min()
95 | delays_samples -= offset_samples
96 | out = _np.zeros((delays_samples.max() + len(data), len(delays_samples)))
97 | for column, row in enumerate(delays_samples):
98 | out[row:row + len(data), column] = data
99 | return _util.DelayedSignal(out, samplerate, offset_samples / samplerate)
100 |
101 |
102 | def secondary_source_point(c):
103 | """Create a point source for use in `sfs.td.synthesize()`."""
104 |
105 | def secondary_source(position, _, signal, observation_time, grid):
106 | return source.point(position, signal, observation_time, grid, c=c)
107 |
108 | return secondary_source
109 |
110 |
111 | from . import nfchoa
112 | from . import wfs
113 |
--------------------------------------------------------------------------------
/sfs/td/nfchoa.py:
--------------------------------------------------------------------------------
1 | """Compute NFC-HOA driving functions.
2 |
3 | .. include:: math-definitions.rst
4 |
5 | .. plot::
6 | :context: reset
7 |
8 | import matplotlib.pyplot as plt
9 | import numpy as np
10 | import sfs
11 | from scipy.signal import unit_impulse
12 |
13 | # Plane wave
14 | npw = sfs.util.direction_vector(np.radians(-45))
15 |
16 | # Point source
17 | xs = -1.5, 1.5, 0
18 | rs = np.linalg.norm(xs) # distance from origin
19 | ts = rs / sfs.default.c # time-of-arrival at origin
20 |
21 | # Impulsive excitation
22 | fs = 44100
23 | signal = unit_impulse(512), fs
24 |
25 | # Circular loudspeaker array
26 | N = 32 # number of loudspeakers
27 | R = 1.5 # radius
28 | array = sfs.array.circular(N, R)
29 |
30 | grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02)
31 |
32 | def plot(d, selection, secondary_source, t=0):
33 | p = sfs.td.synthesize(d, selection, array, secondary_source, grid=grid,
34 | observation_time=t)
35 | sfs.plot2d.level(p, grid)
36 | sfs.plot2d.loudspeakers(array.x, array.n, selection * array.a, size=0.15)
37 |
38 | """
39 | import numpy as _np
40 | import scipy.signal as _sig
41 | from scipy.special import eval_legendre as _legendre
42 |
43 | from . import secondary_source_point as _secondary_source_point
44 | from .. import default as _default
45 | from .. import util as _util
46 |
47 |
48 | def matchedz_zpk(s_zeros, s_poles, s_gain, fs):
49 | """Matched-z transform of poles and zeros.
50 |
51 | Parameters
52 | ----------
53 | s_zeros : array_like
54 | Zeros in the Laplace domain.
55 | s_poles : array_like
56 | Poles in the Laplace domain.
57 | s_gain : float
58 | System gain in the Laplace domain.
59 | fs : int
60 | Sampling frequency in Hertz.
61 |
62 | Returns
63 | -------
64 | z_zeros : numpy.ndarray
65 | Zeros in the z-domain.
66 | z_poles : numpy.ndarray
67 | Poles in the z-domain.
68 | z_gain : float
69 | System gain in the z-domain.
70 |
71 | See Also
72 | --------
73 | :func:`scipy.signal.bilinear_zpk`
74 |
75 | """
76 | z_zeros = _np.exp(s_zeros / fs)
77 | z_poles = _np.exp(s_poles / fs)
78 | omega = 1j * _np.pi * fs
79 | s_gain *= _np.prod((omega - s_zeros) / (omega - s_poles)
80 | * (-1 - z_poles) / (-1 - z_zeros))
81 | return z_zeros, z_poles, _np.real(s_gain)
82 |
83 |
84 | def plane_25d(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk):
85 | r"""Virtual plane wave by 2.5-dimensional NFC-HOA.
86 |
87 | .. math::
88 |
89 | D(\phi_0, s) =
90 | 2\e{\frac{s}{c}r_0}
91 | \sum_{m=-M}^{M}
92 | (-1)^m
93 | \Big(\frac{s}{s-\frac{c}{r_0}\sigma_0}\Big)^\mu
94 | \prod_{l=1}^{\nu}
95 | \frac{s^2}{(s-\frac{c}{r_0}\sigma_l)^2+(\frac{c}{r_0}\omega_l)^2}
96 | \e{\i m(\phi_0 - \phi_\text{pw})}
97 |
98 | The driving function is represented in the Laplace domain,
99 | from which the recursive filters are designed.
100 | :math:`\sigma_l + \i\omega_l` denotes the complex roots of
101 | the reverse Bessel polynomial.
102 | The number of second-order sections is
103 | :math:`\nu = \big\lfloor\tfrac{|m|}{2}\big\rfloor`,
104 | whereas the number of first-order section :math:`\mu` is either 0 or 1
105 | for even and odd :math:`|m|`, respectively.
106 |
107 | Parameters
108 | ----------
109 | x0 : (N, 3) array_like
110 | Sequence of secondary source positions.
111 | r0 : float
112 | Radius of the circular secondary source distribution.
113 | npw : (3,) array_like
114 | Unit vector (propagation direction) of plane wave.
115 | fs : int
116 | Sampling frequency in Hertz.
117 | max_order : int, optional
118 | Ambisonics order.
119 | c : float, optional
120 | Speed of sound in m/s.
121 | s2z : callable, optional
122 | Function transforming s-domain poles and zeros into z-domain,
123 | e.g. :func:`matchedz_zpk`, :func:`scipy.signal.bilinear_zpk`.
124 |
125 | Returns
126 | -------
127 | delay : float
128 | Overall delay in seconds.
129 | weight : float
130 | Overall weight.
131 | sos : list of numpy.ndarray
132 | Second-order section filters :func:`scipy.signal.sosfilt`.
133 | phaseshift : (N,) numpy.ndarray
134 | Phase shift in radians.
135 | selection : (N,) numpy.ndarray
136 | Boolean array containing only ``True`` indicating that
137 | all secondary source are "active" for NFC-HOA.
138 | secondary_source_function : callable
139 | A function that can be used to create the sound field of a
140 | single secondary source. See `sfs.td.synthesize()`.
141 |
142 | Examples
143 | --------
144 | .. plot::
145 | :context: close-figs
146 |
147 | delay, weight, sos, phaseshift, selection, secondary_source = \
148 | sfs.td.nfchoa.plane_25d(array.x, R, npw, fs)
149 | d = sfs.td.nfchoa.driving_signals_25d(
150 | delay, weight, sos, phaseshift, signal)
151 | plot(d, selection, secondary_source)
152 |
153 | """
154 | if max_order is None:
155 | max_order = _util.max_order_circular_harmonics(len(x0))
156 | if c is None:
157 | c = _default.c
158 |
159 | x0 = _util.asarray_of_rows(x0)
160 | npw = _util.asarray_1d(npw)
161 | phi0, _, _ = _util.cart2sph(*x0.T)
162 | phipw, _, _ = _util.cart2sph(*npw)
163 | phaseshift = phi0 - phipw + _np.pi
164 |
165 | delay = -r0 / c
166 | weight = 2
167 | sos = []
168 | for m in range(max_order + 1):
169 | _, p, _ = _sig.besselap(m, norm='delay')
170 | s_zeros = _np.zeros(m)
171 | s_poles = c / r0 * p
172 | s_gain = 1
173 | z_zeros, z_poles, z_gain = s2z(s_zeros, s_poles, s_gain, fs)
174 | sos.append(_sig.zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest'))
175 | selection = _util.source_selection_all(len(x0))
176 | return (delay, weight, sos, phaseshift, selection,
177 | _secondary_source_point(c))
178 |
179 |
180 | def point_25d(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk):
181 | r"""Virtual Point source by 2.5-dimensional NFC-HOA.
182 |
183 | .. math::
184 |
185 | D(\phi_0, s) =
186 | \frac{1}{2\pi r_\text{s}}
187 | \e{\frac{s}{c}(r_0-r_\text{s})}
188 | \sum_{m=-M}^{M}
189 | \Big(\frac{s-\frac{c}{r_\text{s}}\sigma_0}{s-\frac{c}{r_0}\sigma_0}\Big)^\mu
190 | \prod_{l=1}^{\nu}
191 | \frac{(s-\frac{c}{r_\text{s}}\sigma_l)^2-(\frac{c}{r_\text{s}}\omega_l)^2}
192 | {(s-\frac{c}{r_0}\sigma_l)^2+(\frac{c}{r_0}\omega_l)^2}
193 | \e{\i m(\phi_0 - \phi_\text{s})}
194 |
195 | The driving function is represented in the Laplace domain,
196 | from which the recursive filters are designed.
197 | :math:`\sigma_l + \i\omega_l` denotes the complex roots of
198 | the reverse Bessel polynomial.
199 | The number of second-order sections is
200 | :math:`\nu = \big\lfloor\tfrac{|m|}{2}\big\rfloor`,
201 | whereas the number of first-order section :math:`\mu` is either 0 or 1
202 | for even and odd :math:`|m|`, respectively.
203 |
204 | Parameters
205 | ----------
206 | x0 : (N, 3) array_like
207 | Sequence of secondary source positions.
208 | r0 : float
209 | Radius of the circular secondary source distribution.
210 | xs : (3,) array_like
211 | Virtual source position.
212 | fs : int
213 | Sampling frequency in Hertz.
214 | max_order : int, optional
215 | Ambisonics order.
216 | c : float, optional
217 | Speed of sound in m/s.
218 | s2z : callable, optional
219 | Function transforming s-domain poles and zeros into z-domain,
220 | e.g. :func:`matchedz_zpk`, :func:`scipy.signal.bilinear_zpk`.
221 |
222 | Returns
223 | -------
224 | delay : float
225 | Overall delay in seconds.
226 | weight : float
227 | Overall weight.
228 | sos : list of numpy.ndarray
229 | Second-order section filters :func:`scipy.signal.sosfilt`.
230 | phaseshift : (N,) numpy.ndarray
231 | Phase shift in radians.
232 | selection : (N,) numpy.ndarray
233 | Boolean array containing only ``True`` indicating that
234 | all secondary source are "active" for NFC-HOA.
235 | secondary_source_function : callable
236 | A function that can be used to create the sound field of a
237 | single secondary source. See `sfs.td.synthesize()`.
238 |
239 | Examples
240 | --------
241 | .. plot::
242 | :context: close-figs
243 |
244 | delay, weight, sos, phaseshift, selection, secondary_source = \
245 | sfs.td.nfchoa.point_25d(array.x, R, xs, fs)
246 | d = sfs.td.nfchoa.driving_signals_25d(
247 | delay, weight, sos, phaseshift, signal)
248 | plot(d, selection, secondary_source, t=ts)
249 |
250 | """
251 | if max_order is None:
252 | max_order = _util.max_order_circular_harmonics(len(x0))
253 | if c is None:
254 | c = _default.c
255 |
256 | x0 = _util.asarray_of_rows(x0)
257 | xs = _util.asarray_1d(xs)
258 | phi0, _, _ = _util.cart2sph(*x0.T)
259 | phis, _, rs = _util.cart2sph(*xs)
260 | phaseshift = phi0 - phis
261 |
262 | delay = (rs - r0) / c
263 | weight = 1 / 2 / _np.pi / rs
264 | sos = []
265 | for m in range(max_order + 1):
266 | _, p, _ = _sig.besselap(m, norm='delay')
267 | s_zeros = c / rs * p
268 | s_poles = c / r0 * p
269 | s_gain = 1
270 | z_zeros, z_poles, z_gain = s2z(s_zeros, s_poles, s_gain, fs)
271 | sos.append(_sig.zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest'))
272 | selection = _util.source_selection_all(len(x0))
273 | return (delay, weight, sos, phaseshift, selection,
274 | _secondary_source_point(c))
275 |
276 |
277 | def plane_3d(x0, r0, npw, fs, max_order=None, c=None, s2z=matchedz_zpk):
278 | r"""Virtual plane wave by 3-dimensional NFC-HOA.
279 |
280 | .. math::
281 |
282 | D(\phi_0, s) =
283 | \frac{\e{\frac{s}{c}r_0}}{r_0}
284 | \sum_{n=0}^{N}
285 | (-1)^n (2n+1) P_{n}(\cos\Theta)
286 | \Big(\frac{s}{s-\frac{c}{r_0}\sigma_0}\Big)^\mu
287 | \prod_{l=1}^{\nu}
288 | \frac{s^2}{(s-\frac{c}{r_0}\sigma_l)^2+(\frac{c}{r_0}\omega_l)^2}
289 |
290 | The driving function is represented in the Laplace domain,
291 | from which the recursive filters are designed.
292 | :math:`\sigma_l + \i\omega_l` denotes the complex roots of
293 | the reverse Bessel polynomial.
294 | The number of second-order sections is
295 | :math:`\nu = \big\lfloor\tfrac{|m|}{2}\big\rfloor`,
296 | whereas the number of first-order section :math:`\mu` is either 0 or 1
297 | for even and odd :math:`|m|`, respectively.
298 | :math:`P_{n}(\cdot)` denotes the Legendre polynomial of degree :math:`n`,
299 | and :math:`\Theta` the angle between :math:`(\theta, \phi)`
300 | and :math:`(\theta_\text{pw}, \phi_\text{pw})`.
301 |
302 | Parameters
303 | ----------
304 | x0 : (N, 3) array_like
305 | Sequence of secondary source positions.
306 | r0 : float
307 | Radius of the spherical secondary source distribution.
308 | npw : (3,) array_like
309 | Unit vector (propagation direction) of plane wave.
310 | fs : int
311 | Sampling frequency in Hertz.
312 | max_order : int, optional
313 | Ambisonics order.
314 | c : float, optional
315 | Speed of sound in m/s.
316 | s2z : callable, optional
317 | Function transforming s-domain poles and zeros into z-domain,
318 | e.g. :func:`matchedz_zpk`, :func:`scipy.signal.bilinear_zpk`.
319 |
320 | Returns
321 | -------
322 | delay : float
323 | Overall delay in seconds.
324 | weight : float
325 | Overall weight.
326 | sos : list of numpy.ndarray
327 | Second-order section filters :func:`scipy.signal.sosfilt`.
328 | phaseshift : (N,) numpy.ndarray
329 | Phase shift in radians.
330 | selection : (N,) numpy.ndarray
331 | Boolean array containing only ``True`` indicating that
332 | all secondary source are "active" for NFC-HOA.
333 | secondary_source_function : callable
334 | A function that can be used to create the sound field of a
335 | single secondary source. See `sfs.td.synthesize()`.
336 |
337 | """
338 | if max_order is None:
339 | max_order = _util.max_order_spherical_harmonics(len(x0))
340 | if c is None:
341 | c = _default.c
342 |
343 | x0 = _util.asarray_of_rows(x0)
344 | npw = _util.asarray_1d(npw)
345 | phi0, theta0, _ = _util.cart2sph(*x0.T)
346 | phipw, thetapw, _ = _util.cart2sph(*npw)
347 | phaseshift = _np.arccos(_np.dot(x0 / r0, -npw))
348 |
349 | delay = -r0 / c
350 | weight = 4 * _np.pi / r0
351 | sos = []
352 | for m in range(max_order + 1):
353 | _, p, _ = _sig.besselap(m, norm='delay')
354 | s_zeros = _np.zeros(m)
355 | s_poles = c / r0 * p
356 | s_gain = 1
357 | z_zeros, z_poles, z_gain = s2z(s_zeros, s_poles, s_gain, fs)
358 | sos.append(_sig.zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest'))
359 | selection = _util.source_selection_all(len(x0))
360 | return (delay, weight, sos, phaseshift, selection,
361 | _secondary_source_point(c))
362 |
363 |
364 | def point_3d(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk):
365 | r"""Virtual point source by 3-dimensional NFC-HOA.
366 |
367 | .. math::
368 |
369 | D(\phi_0, s) =
370 | \frac{\e{\frac{s}{c}(r_0-r_\text{s})}}{4 \pi r_0 r_\text{s}}
371 | \sum_{n=0}^{N}
372 | (2n+1) P_{n}(\cos\Theta)
373 | \Big(\frac{s-\frac{c}{r_\text{s}}\sigma_0}{s-\frac{c}{r_0}\sigma_0}\Big)^\mu
374 | \prod_{l=1}^{\nu}
375 | \frac{(s-\frac{c}{r_\text{s}}\sigma_l)^2-(\frac{c}{r_\text{s}}\omega_l)^2}
376 | {(s-\frac{c}{r_0}\sigma_l)^2+(\frac{c}{r_0}\omega_l)^2}
377 |
378 | The driving function is represented in the Laplace domain,
379 | from which the recursive filters are designed.
380 | :math:`\sigma_l + \i\omega_l` denotes the complex roots of
381 | the reverse Bessel polynomial.
382 | The number of second-order sections is
383 | :math:`\nu = \big\lfloor\tfrac{|m|}{2}\big\rfloor`,
384 | whereas the number of first-order section :math:`\mu` is either 0 or 1
385 | for even and odd :math:`|m|`, respectively.
386 | :math:`P_{n}(\cdot)` denotes the Legendre polynomial of degree :math:`n`,
387 | and :math:`\Theta` the angle between :math:`(\theta, \phi)`
388 | and :math:`(\theta_\text{s}, \phi_\text{s})`.
389 |
390 | Parameters
391 | ----------
392 | x0 : (N, 3) array_like
393 | Sequence of secondary source positions.
394 | r0 : float
395 | Radius of the spherial secondary source distribution.
396 | xs : (3,) array_like
397 | Virtual source position.
398 | fs : int
399 | Sampling frequency in Hertz.
400 | max_order : int, optional
401 | Ambisonics order.
402 | c : float, optional
403 | Speed of sound in m/s.
404 | s2z : callable, optional
405 | Function transforming s-domain poles and zeros into z-domain,
406 | e.g. :func:`matchedz_zpk`, :func:`scipy.signal.bilinear_zpk`.
407 |
408 | Returns
409 | -------
410 | delay : float
411 | Overall delay in seconds.
412 | weight : float
413 | Overall weight.
414 | sos : list of numpy.ndarray
415 | Second-order section filters :func:`scipy.signal.sosfilt`.
416 | phaseshift : (N,) numpy.ndarray
417 | Phase shift in radians.
418 | selection : (N,) numpy.ndarray
419 | Boolean array containing only ``True`` indicating that
420 | all secondary source are "active" for NFC-HOA.
421 | secondary_source_function : callable
422 | A function that can be used to create the sound field of a
423 | single secondary source. See `sfs.td.synthesize()`.
424 |
425 | """
426 | if max_order is None:
427 | max_order = _util.max_order_spherical_harmonics(len(x0))
428 | if c is None:
429 | c = _default.c
430 |
431 | x0 = _util.asarray_of_rows(x0)
432 | xs = _util.asarray_1d(xs)
433 | phi0, theta0, _ = _util.cart2sph(*x0.T)
434 | phis, thetas, rs = _util.cart2sph(*xs)
435 | phaseshift = _np.arccos(_np.dot(x0 / r0, xs / rs))
436 |
437 | delay = (rs - r0) / c
438 | weight = 1 / r0 / rs
439 | sos = []
440 | for m in range(max_order + 1):
441 | _, p, _ = _sig.besselap(m, norm='delay')
442 | s_zeros = c / rs * p
443 | s_poles = c / r0 * p
444 | s_gain = 1
445 | z_zeros, z_poles, z_gain = s2z(s_zeros, s_poles, s_gain, fs)
446 | sos.append(_sig.zpk2sos(z_zeros, z_poles, z_gain, pairing='nearest'))
447 | selection = _util.source_selection_all(len(x0))
448 | return (delay, weight, sos, phaseshift, selection,
449 | _secondary_source_point(c))
450 |
451 |
452 | def driving_signals_25d(delay, weight, sos, phaseshift, signal):
453 | """Get 2.5-dimensional NFC-HOA driving signals.
454 |
455 | Parameters
456 | ----------
457 | delay : float
458 | Overall delay in seconds.
459 | weight : float
460 | Overall weight.
461 | sos : list of array_like
462 | Second-order section filters :func:`scipy.signal.sosfilt`.
463 | phaseshift : (N,) array_like
464 | Phase shift in radians.
465 | signal : (L,) array_like + float
466 | Excitation signal consisting of (mono) audio data and a sampling
467 | rate (in Hertz). A `DelayedSignal` object can also be used.
468 |
469 | Returns
470 | -------
471 | `DelayedSignal`
472 | A tuple containing the delayed signals (in a `numpy.ndarray`
473 | with shape ``(L, N)``), followed by the sampling rate (in Hertz)
474 | and a (possibly negative) time offset (in seconds).
475 |
476 | """
477 | data, fs, t_offset = _util.as_delayed_signal(signal)
478 | N = len(phaseshift)
479 | out = _np.tile(_np.expand_dims(_sig.sosfilt(sos[0], data), 1), (1, N))
480 | for m in range(1, len(sos)):
481 | modal_response = _sig.sosfilt(sos[m], data)[:, _np.newaxis]
482 | out += modal_response * _np.cos(m * phaseshift)
483 | return _util.DelayedSignal(2 * weight * out, fs, t_offset + delay)
484 |
485 |
486 | def driving_signals_3d(delay, weight, sos, phaseshift, signal):
487 | """Get 3-dimensional NFC-HOA driving signals.
488 |
489 | Parameters
490 | ----------
491 | delay : float
492 | Overall delay in seconds.
493 | weight : float
494 | Overall weight.
495 | sos : list of array_like
496 | Second-order section filters :func:`scipy.signal.sosfilt`.
497 | phaseshift : (N,) array_like
498 | Phase shift in radians.
499 | signal : (L,) array_like + float
500 | Excitation signal consisting of (mono) audio data and a sampling
501 | rate (in Hertz). A `DelayedSignal` object can also be used.
502 |
503 | Returns
504 | -------
505 | `DelayedSignal`
506 | A tuple containing the delayed signals (in a `numpy.ndarray`
507 | with shape ``(L, N)``), followed by the sampling rate (in Hertz)
508 | and a (possibly negative) time offset (in seconds).
509 |
510 | """
511 | data, fs, t_offset = _util.as_delayed_signal(signal)
512 | N = len(phaseshift)
513 | out = _np.tile(_np.expand_dims(_sig.sosfilt(sos[0], data), 1), (1, N))
514 | for m in range(1, len(sos)):
515 | modal_response = _sig.sosfilt(sos[m], data)[:, _np.newaxis]
516 | out += (2 * m + 1) * modal_response * _legendre(m, _np.cos(phaseshift))
517 | return _util.DelayedSignal(weight / 4 / _np.pi * out, fs, t_offset + delay)
518 |
--------------------------------------------------------------------------------
/sfs/td/source.py:
--------------------------------------------------------------------------------
1 | """Compute the sound field generated by a sound source.
2 |
3 | The Green's function describes the spatial sound propagation over time.
4 |
5 | .. include:: math-definitions.rst
6 |
7 | .. plot::
8 | :context: reset
9 |
10 | import matplotlib.pyplot as plt
11 | import numpy as np
12 | from scipy.signal import unit_impulse
13 | import sfs
14 |
15 | xs = 1.5, 1, 0 # source position
16 | rs = np.linalg.norm(xs) # distance from origin
17 | ts = rs / sfs.default.c # time-of-arrival at origin
18 |
19 | # Impulsive excitation
20 | fs = 44100
21 | signal = unit_impulse(512), fs
22 |
23 | grid = sfs.util.xyz_grid([-2, 3], [-1, 2], 0, spacing=0.02)
24 |
25 | """
26 | import numpy as _np
27 |
28 | from .. import default as _default
29 | from .. import util as _util
30 |
31 |
32 | def point(xs, signal, observation_time, grid, c=None):
33 | r"""Source model for a point source: 3D Green's function.
34 |
35 | Calculates the scalar sound pressure field for a given point in
36 | time, evoked by source excitation signal.
37 |
38 | Parameters
39 | ----------
40 | xs : (3,) array_like
41 | Position of source in cartesian coordinates.
42 | signal : (N,) array_like + float
43 | Excitation signal consisting of (mono) audio data and a sampling
44 | rate (in Hertz). A `DelayedSignal` object can also be used.
45 | observation_time : float
46 | Observed point in time.
47 | grid : triple of array_like
48 | The grid that is used for the sound field calculations.
49 | See `sfs.util.xyz_grid()`.
50 | c : float, optional
51 | Speed of sound.
52 |
53 | Returns
54 | -------
55 | numpy.ndarray
56 | Scalar sound pressure field, evaluated at positions given by
57 | *grid*.
58 |
59 | Notes
60 | -----
61 | .. math::
62 |
63 | g(x-x_s,t) = \frac{1}{4 \pi |x - x_s|} \dirac{t - \frac{|x -
64 | x_s|}{c}}
65 |
66 | Examples
67 | --------
68 | .. plot::
69 | :context: close-figs
70 |
71 | p = sfs.td.source.point(xs, signal, ts, grid)
72 | sfs.plot2d.level(p, grid)
73 |
74 | """
75 | xs = _util.asarray_1d(xs)
76 | data, samplerate, signal_offset = _util.as_delayed_signal(signal)
77 | data = _util.asarray_1d(data)
78 | grid = _util.as_xyz_components(grid)
79 | if c is None:
80 | c = _default.c
81 | r = _np.linalg.norm(grid - xs)
82 | # If r is +-0, the sound pressure is +-infinity
83 | with _np.errstate(divide='ignore'):
84 | weights = 1 / (4 * _np.pi * r)
85 | delays = r / c
86 | base_time = observation_time - signal_offset
87 | points_at_time = _np.interp(base_time - delays,
88 | _np.arange(len(data)) / samplerate,
89 | data, left=0, right=0)
90 | # weights can be +-infinity
91 | with _np.errstate(invalid='ignore'):
92 | return weights * points_at_time
93 |
94 |
95 | def point_image_sources(x0, signal, observation_time, grid, L, max_order,
96 | coeffs=None, c=None):
97 | """Point source in a rectangular room using the mirror image source model.
98 |
99 | Parameters
100 | ----------
101 | x0 : (3,) array_like
102 | Position of source in cartesian coordinates.
103 | signal : (N,) array_like + float
104 | Excitation signal consisting of (mono) audio data and a sampling
105 | rate (in Hertz). A `DelayedSignal` object can also be used.
106 | observation_time : float
107 | Observed point in time.
108 | grid : triple of array_like
109 | The grid that is used for the sound field calculations.
110 | See `sfs.util.xyz_grid()`.
111 | L : (3,) array_like
112 | Dimensions of the rectangular room.
113 | max_order : int
114 | Maximum number of reflections for each image source.
115 | coeffs : (6,) array_like, optional
116 | Reflection coeffecients of the walls.
117 | If not given, the reflection coefficients are set to one.
118 | c : float, optional
119 | Speed of sound.
120 |
121 | Returns
122 | -------
123 | numpy.ndarray
124 | Scalar sound pressure field, evaluated at positions given by
125 | *grid*.
126 |
127 | Examples
128 | --------
129 | .. plot::
130 | :context: close-figs
131 |
132 | room = 5, 3, 1.5 # room dimensions
133 | order = 2 # image source order
134 | coeffs = .8, .8, .6, .6, .7, .7 # wall reflection coefficients
135 | grid = sfs.util.xyz_grid([0, room[0]], [0, room[1]], 0, spacing=0.01)
136 | p = sfs.td.source.point_image_sources(
137 | xs, signal, 1.5 * ts, grid, room, order, coeffs)
138 | sfs.plot2d.level(p, grid)
139 |
140 | """
141 | if coeffs is None:
142 | coeffs = _np.ones(6)
143 |
144 | positions, order = _util.image_sources_for_box(x0, L, max_order)
145 | source_strengths = _np.prod(coeffs**order, axis=1)
146 |
147 | p = 0
148 | for position, strength in zip(positions, source_strengths):
149 | if strength != 0:
150 | p += strength * point(position, signal, observation_time, grid, c)
151 |
152 | return p
153 |
--------------------------------------------------------------------------------
/sfs/td/wfs.py:
--------------------------------------------------------------------------------
1 | """Compute WFS driving functions.
2 |
3 | .. include:: math-definitions.rst
4 |
5 | .. plot::
6 | :context: reset
7 |
8 | import matplotlib.pyplot as plt
9 | import numpy as np
10 | import sfs
11 | from scipy.signal import unit_impulse
12 |
13 | # Plane wave
14 | npw = sfs.util.direction_vector(np.radians(-45))
15 |
16 | # Point source
17 | xs = -1.5, 1.5, 0
18 | rs = np.linalg.norm(xs) # distance from origin
19 | ts = rs / sfs.default.c # time-of-arrival at origin
20 |
21 | # Focused source
22 | xf = -0.5, 0.5, 0
23 | nf = sfs.util.direction_vector(np.radians(-45)) # normal vector
24 | rf = np.linalg.norm(xf) # distance from origin
25 | tf = rf / sfs.default.c # time-of-arrival at origin
26 |
27 | # Impulsive excitation
28 | fs = 44100
29 | signal = unit_impulse(512), fs
30 |
31 | # Circular loudspeaker array
32 | N = 32 # number of loudspeakers
33 | R = 1.5 # radius
34 | array = sfs.array.circular(N, R)
35 |
36 | grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02)
37 |
38 | def plot(d, selection, secondary_source, t=0):
39 | p = sfs.td.synthesize(d, selection, array, secondary_source, grid=grid,
40 | observation_time=t)
41 | sfs.plot2d.level(p, grid)
42 | sfs.plot2d.loudspeakers(array.x, array.n,
43 | selection * array.a, size=0.15)
44 |
45 | """
46 | import numpy as _np
47 | from numpy.core.umath_tests import inner1d as _inner1d
48 |
49 | from . import apply_delays as _apply_delays
50 | from . import secondary_source_point as _secondary_source_point
51 | from .. import default as _default
52 | from .. import util as _util
53 |
54 |
55 | def plane_25d(x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None):
56 | r"""Plane wave model by 2.5-dimensional WFS.
57 |
58 | Parameters
59 | ----------
60 | x0 : (N, 3) array_like
61 | Sequence of secondary source positions.
62 | n0 : (N, 3) array_like
63 | Sequence of secondary source orientations.
64 | n : (3,) array_like, optional
65 | Normal vector (propagation direction) of synthesized plane wave.
66 | xref : (3,) array_like, optional
67 | Reference position
68 | c : float, optional
69 | Speed of sound
70 |
71 | Returns
72 | -------
73 | delays : (N,) numpy.ndarray
74 | Delays of secondary sources in seconds.
75 | weights : (N,) numpy.ndarray
76 | Weights of secondary sources.
77 | selection : (N,) numpy.ndarray
78 | Boolean array containing ``True`` or ``False`` depending on
79 | whether the corresponding secondary source is "active" or not.
80 | secondary_source_function : callable
81 | A function that can be used to create the sound field of a
82 | single secondary source. See `sfs.td.synthesize()`.
83 |
84 | Notes
85 | -----
86 | 2.5D correction factor
87 |
88 | .. math::
89 |
90 | g_0 = \sqrt{2 \pi |x_\mathrm{ref} - x_0|}
91 |
92 | d using a plane wave as source model
93 |
94 | .. math::
95 |
96 | d_{2.5D}(x_0,t) =
97 | 2 g_0 \scalarprod{n}{n_0}
98 | \dirac{t - \frac{1}{c} \scalarprod{n}{x_0}} \ast_t h(t)
99 |
100 | with wfs(2.5D) prefilter h(t), which is not implemented yet.
101 |
102 | See :sfs:`d_wfs/#equation-td-wfs-plane-25d`
103 |
104 | Examples
105 | --------
106 | .. plot::
107 | :context: close-figs
108 |
109 | delays, weights, selection, secondary_source = \
110 | sfs.td.wfs.plane_25d(array.x, array.n, npw)
111 | d = sfs.td.wfs.driving_signals(delays, weights, signal)
112 | plot(d, selection, secondary_source)
113 |
114 | """
115 | if c is None:
116 | c = _default.c
117 | x0 = _util.asarray_of_rows(x0)
118 | n0 = _util.asarray_of_rows(n0)
119 | n = _util.normalize_vector(n)
120 | xref = _util.asarray_1d(xref)
121 | g0 = _np.sqrt(2 * _np.pi * _np.linalg.norm(xref - x0, axis=1))
122 | delays = _inner1d(n, x0) / c
123 | weights = 2 * g0 * _inner1d(n, n0)
124 | selection = _util.source_selection_plane(n0, n)
125 | return delays, weights, selection, _secondary_source_point(c)
126 |
127 |
128 | def point_25d(x0, n0, xs, xref=[0, 0, 0], c=None):
129 | r"""Driving function for 2.5-dimensional WFS of a virtual point source.
130 |
131 | .. versionchanged:: 0.6.1
132 | see notes, old handling of `point_25d()` is now `point_25d_legacy()`
133 |
134 | Parameters
135 | ----------
136 | x0 : (N, 3) array_like
137 | Sequence of secondary source positions.
138 | n0 : (N, 3) array_like
139 | Sequence of secondary source orientations.
140 | xs : (3,) array_like
141 | Virtual source position.
142 | xref : (N, 3) array_like or (3,) array_like
143 | Contour xref(x0) for amplitude correct synthesis, reference point xref.
144 | c : float, optional
145 | Speed of sound
146 |
147 | Returns
148 | -------
149 | delays : (N,) numpy.ndarray
150 | Delays of secondary sources in seconds.
151 | weights: (N,) numpy.ndarray
152 | Weights of secondary sources.
153 | selection : (N,) numpy.ndarray
154 | Boolean array containing ``True`` or ``False`` depending on
155 | whether the corresponding secondary source is "active" or not.
156 | secondary_source_function : callable
157 | A function that can be used to create the sound field of a
158 | single secondary source. See `sfs.td.synthesize()`.
159 |
160 | Notes
161 | -----
162 | Eq. (2.138) in :cite:`Schultz2016`:
163 |
164 | .. math::
165 |
166 | d_{2.5D}(x_0, x_{ref}, t) =
167 | \sqrt{8\pi}
168 | \frac{\scalarprod{(x_0 - x_s)}{n_0}}{|x_0 - x_s|}
169 | \sqrt{\frac{|x_0 - x_s||x_0 - x_{ref}|}{|x_0 - x_s|+|x_0 - x_{ref}|}}
170 | \cdot
171 | \frac{\dirac{t - \frac{|x_0 - x_s|}{c}}}{4\pi |x_0 - x_s|} \ast_t h(t)
172 |
173 | .. math::
174 |
175 | h(t) = F^{-1}(\sqrt{\frac{j \omega}{c}})
176 |
177 | with wfs(2.5D) prefilter h(t), which is not implemented yet.
178 |
179 | `point_25d()` derives WFS from 3D to 2.5D via the stationary phase
180 | approximation approach (i.e. the Delft approach).
181 | The theoretical link of `point_25d()` and `point_25d_legacy()` was
182 | introduced as *unified WFS framework* in :cite:`Firtha2017`.
183 |
184 | Examples
185 | --------
186 | .. plot::
187 | :context: close-figs
188 |
189 | delays, weights, selection, secondary_source = \
190 | sfs.td.wfs.point_25d(array.x, array.n, xs)
191 | d = sfs.td.wfs.driving_signals(delays, weights, signal)
192 | plot(d, selection, secondary_source, t=ts)
193 |
194 | """
195 | if c is None:
196 | c = _default.c
197 | x0 = _util.asarray_of_rows(x0)
198 | n0 = _util.asarray_of_rows(n0)
199 | xs = _util.asarray_1d(xs)
200 | xref = _util.asarray_of_rows(xref)
201 |
202 | x0xs = x0 - xs
203 | x0xref = x0 - xref
204 | x0xs_n = _np.linalg.norm(x0xs, axis=1)
205 | x0xref_n = _np.linalg.norm(x0xref, axis=1)
206 |
207 | g0 = 1/(_np.sqrt(2*_np.pi)*x0xs_n**2)
208 | g0 *= _np.sqrt((x0xs_n*x0xref_n)/(x0xs_n+x0xref_n))
209 |
210 | delays = x0xs_n/c
211 | weights = g0*_inner1d(x0xs, n0)
212 | selection = _util.source_selection_point(n0, x0, xs)
213 | return delays, weights, selection, _secondary_source_point(c)
214 |
215 |
216 | def point_25d_legacy(x0, n0, xs, xref=[0, 0, 0], c=None):
217 | r"""Driving function for 2.5-dimensional WFS of a virtual point source.
218 |
219 | .. versionadded:: 0.6.1
220 | `point_25d()` was renamed to `point_25d_legacy()` (and a new
221 | function with the name `point_25d()` was introduced). See notes below
222 | for further details.
223 |
224 | Parameters
225 | ----------
226 | x0 : (N, 3) array_like
227 | Sequence of secondary source positions.
228 | n0 : (N, 3) array_like
229 | Sequence of secondary source orientations.
230 | xs : (3,) array_like
231 | Virtual source position.
232 | xref : (3,) array_like, optional
233 | Reference position
234 | c : float, optional
235 | Speed of sound
236 |
237 | Returns
238 | -------
239 | delays : (N,) numpy.ndarray
240 | Delays of secondary sources in seconds.
241 | weights: (N,) numpy.ndarray
242 | Weights of secondary sources.
243 | selection : (N,) numpy.ndarray
244 | Boolean array containing ``True`` or ``False`` depending on
245 | whether the corresponding secondary source is "active" or not.
246 | secondary_source_function : callable
247 | A function that can be used to create the sound field of a
248 | single secondary source. See `sfs.td.synthesize()`.
249 |
250 | Notes
251 | -----
252 | 2.5D correction factor
253 |
254 | .. math::
255 |
256 | g_0 = \sqrt{2 \pi |x_\mathrm{ref} - x_0|}
257 |
258 |
259 | d using a point source as source model
260 |
261 | .. math::
262 |
263 | d_{2.5D}(x_0,t) =
264 | \frac{g_0 \scalarprod{(x_0 - x_s)}{n_0}}
265 | {2\pi |x_0 - x_s|^{3/2}}
266 | \dirac{t - \frac{|x_0 - x_s|}{c}} \ast_t h(t)
267 |
268 | with wfs(2.5D) prefilter h(t), which is not implemented yet.
269 |
270 | See :sfs:`d_wfs/#equation-td-wfs-point-25d`
271 |
272 | `point_25d_legacy()` derives 2.5D WFS from the 2D
273 | Neumann-Rayleigh integral (i.e. the approach by Rabenstein & Spors), cf.
274 | :cite:`Spors2008`.
275 | The theoretical link of `point_25d()` and `point_25d_legacy()` was
276 | introduced as *unified WFS framework* in :cite:`Firtha2017`.
277 |
278 | Examples
279 | --------
280 | .. plot::
281 | :context: close-figs
282 |
283 | delays, weights, selection, secondary_source = \
284 | sfs.td.wfs.point_25d(array.x, array.n, xs)
285 | d = sfs.td.wfs.driving_signals(delays, weights, signal)
286 | plot(d, selection, secondary_source, t=ts)
287 |
288 | """
289 | if c is None:
290 | c = _default.c
291 | x0 = _util.asarray_of_rows(x0)
292 | n0 = _util.asarray_of_rows(n0)
293 | xs = _util.asarray_1d(xs)
294 | xref = _util.asarray_1d(xref)
295 | g0 = _np.sqrt(2 * _np.pi * _np.linalg.norm(xref - x0, axis=1))
296 | ds = x0 - xs
297 | r = _np.linalg.norm(ds, axis=1)
298 | delays = r/c
299 | weights = g0 * _inner1d(ds, n0) / (2 * _np.pi * r**(3/2))
300 | selection = _util.source_selection_point(n0, x0, xs)
301 | return delays, weights, selection, _secondary_source_point(c)
302 |
303 |
304 | def focused_25d(x0, n0, xs, ns, xref=[0, 0, 0], c=None):
305 | r"""Point source by 2.5-dimensional WFS.
306 |
307 | Parameters
308 | ----------
309 | x0 : (N, 3) array_like
310 | Sequence of secondary source positions.
311 | n0 : (N, 3) array_like
312 | Sequence of secondary source orientations.
313 | xs : (3,) array_like
314 | Virtual source position.
315 | ns : (3,) array_like
316 | Normal vector (propagation direction) of focused source.
317 | This is used for secondary source selection,
318 | see `sfs.util.source_selection_focused()`.
319 | xref : (3,) array_like, optional
320 | Reference position
321 | c : float, optional
322 | Speed of sound
323 |
324 | Returns
325 | -------
326 | delays : (N,) numpy.ndarray
327 | Delays of secondary sources in seconds.
328 | weights: (N,) numpy.ndarray
329 | Weights of secondary sources.
330 | selection : (N,) numpy.ndarray
331 | Boolean array containing ``True`` or ``False`` depending on
332 | whether the corresponding secondary source is "active" or not.
333 | secondary_source_function : callable
334 | A function that can be used to create the sound field of a
335 | single secondary source. See `sfs.td.synthesize()`.
336 |
337 | Notes
338 | -----
339 | 2.5D correction factor
340 |
341 | .. math::
342 |
343 | g_0 = \sqrt{\frac{|x_\mathrm{ref} - x_0|}
344 | {|x_0-x_s| + |x_\mathrm{ref}-x_0|}}
345 |
346 |
347 | d using a point source as source model
348 |
349 | .. math::
350 |
351 | d_{2.5D}(x_0,t) =
352 | \frac{g_0 \scalarprod{(x_0 - x_s)}{n_0}}
353 | {|x_0 - x_s|^{3/2}}
354 | \dirac{t + \frac{|x_0 - x_s|}{c}} \ast_t h(t)
355 |
356 | with wfs(2.5D) prefilter h(t), which is not implemented yet.
357 |
358 | See :sfs:`d_wfs/#equation-td-wfs-focused-25d`
359 |
360 | Examples
361 | --------
362 | .. plot::
363 | :context: close-figs
364 |
365 | delays, weights, selection, secondary_source = \
366 | sfs.td.wfs.focused_25d(array.x, array.n, xf, nf)
367 | d = sfs.td.wfs.driving_signals(delays, weights, signal)
368 | plot(d, selection, secondary_source, t=tf)
369 |
370 | """
371 | if c is None:
372 | c = _default.c
373 | x0 = _util.asarray_of_rows(x0)
374 | n0 = _util.asarray_of_rows(n0)
375 | xs = _util.asarray_1d(xs)
376 | xref = _util.asarray_1d(xref)
377 | ds = x0 - xs
378 | r = _np.linalg.norm(ds, axis=1)
379 | g0 = _np.sqrt(_np.linalg.norm(xref - x0, axis=1)
380 | / (_np.linalg.norm(xref - x0, axis=1) + r))
381 | delays = -r/c
382 | weights = g0 * _inner1d(ds, n0) / (2 * _np.pi * r**(3/2))
383 | selection = _util.source_selection_focused(ns, x0, xs)
384 | return delays, weights, selection, _secondary_source_point(c)
385 |
386 |
387 | def driving_signals(delays, weights, signal):
388 | """Get driving signals per secondary source.
389 |
390 | Returned signals are the delayed and weighted mono input signal
391 | (with N samples) per channel (C).
392 |
393 | Parameters
394 | ----------
395 | delays : (C,) array_like
396 | Delay in seconds for each channel, negative values allowed.
397 | weights : (C,) array_like
398 | Amplitude weighting factor for each channel.
399 | signal : (N,) array_like + float
400 | Excitation signal consisting of (mono) audio data and a sampling
401 | rate (in Hertz). A `DelayedSignal` object can also be used.
402 |
403 | Returns
404 | -------
405 | `DelayedSignal`
406 | A tuple containing the driving signals (in a `numpy.ndarray`
407 | with shape ``(N, C)``), followed by the sampling rate (in Hertz)
408 | and a (possibly negative) time offset (in seconds).
409 |
410 | """
411 | delays = _util.asarray_1d(delays)
412 | weights = _util.asarray_1d(weights)
413 | data, samplerate, signal_offset = _apply_delays(signal, delays)
414 | return _util.DelayedSignal(data * weights, samplerate, signal_offset)
415 |
--------------------------------------------------------------------------------
/sfs/util.py:
--------------------------------------------------------------------------------
1 | """Various utility functions.
2 |
3 | .. include:: math-definitions.rst
4 |
5 | """
6 |
7 | import collections
8 | import numpy as np
9 | from numpy.core.umath_tests import inner1d
10 | from scipy.special import spherical_jn, spherical_yn
11 | from . import default
12 |
13 |
14 | def rotation_matrix(n1, n2):
15 | """Compute rotation matrix for rotation from *n1* to *n2*.
16 |
17 | Parameters
18 | ----------
19 | n1, n2 : (3,) array_like
20 | Two vectors. They don't have to be normalized.
21 |
22 | Returns
23 | -------
24 | (3, 3) `numpy.ndarray`
25 | Rotation matrix.
26 |
27 | """
28 | n1 = normalize_vector(n1)
29 | n2 = normalize_vector(n2)
30 | I = np.identity(3)
31 | if np.all(n1 == n2):
32 | return I # no rotation
33 | elif np.all(n1 == -n2):
34 | return -I # flip
35 | # TODO: check for *very close to* parallel vectors
36 |
37 | # Algorithm from http://math.stackexchange.com/a/476311
38 | v = v0, v1, v2 = np.cross(n1, n2)
39 | s = np.linalg.norm(v) # sine
40 | c = np.inner(n1, n2) # cosine
41 | vx = [[0, -v2, v1],
42 | [v2, 0, -v0],
43 | [-v1, v0, 0]] # skew-symmetric cross-product matrix
44 | return I + vx + np.dot(vx, vx) * (1 - c) / s**2
45 |
46 |
47 | def wavenumber(omega, c=None):
48 | """Compute the wavenumber for a given radial frequency."""
49 | if c is None:
50 | c = default.c
51 | return omega / c
52 |
53 |
54 | def direction_vector(alpha, beta=np.pi/2):
55 | """Compute normal vector from azimuth, colatitude."""
56 | return sph2cart(alpha, beta, 1)
57 |
58 |
59 | def sph2cart(alpha, beta, r):
60 | r"""Spherical to cartesian coordinate transform.
61 |
62 | .. math::
63 |
64 | x = r \cos \alpha \sin \beta \\
65 | y = r \sin \alpha \sin \beta \\
66 | z = r \cos \beta
67 |
68 | with :math:`\alpha \in [0, 2\pi), \beta \in [0, \pi], r \geq 0`
69 |
70 | Parameters
71 | ----------
72 | alpha : float or array_like
73 | Azimuth angle in radiants
74 | beta : float or array_like
75 | Colatitude angle in radiants (with 0 denoting North pole)
76 | r : float or array_like
77 | Radius
78 |
79 | Returns
80 | -------
81 | x : float or `numpy.ndarray`
82 | x-component of Cartesian coordinates
83 | y : float or `numpy.ndarray`
84 | y-component of Cartesian coordinates
85 | z : float or `numpy.ndarray`
86 | z-component of Cartesian coordinates
87 |
88 | """
89 | x = r * np.cos(alpha) * np.sin(beta)
90 | y = r * np.sin(alpha) * np.sin(beta)
91 | z = r * np.cos(beta)
92 | return x, y, z
93 |
94 |
95 | def cart2sph(x, y, z):
96 | r"""Cartesian to spherical coordinate transform.
97 |
98 | .. math::
99 |
100 | \alpha = \arctan \left( \frac{y}{x} \right) \\
101 | \beta = \arccos \left( \frac{z}{r} \right) \\
102 | r = \sqrt{x^2 + y^2 + z^2}
103 |
104 | with :math:`\alpha \in [-pi, pi], \beta \in [0, \pi], r \geq 0`
105 |
106 | Parameters
107 | ----------
108 | x : float or array_like
109 | x-component of Cartesian coordinates
110 | y : float or array_like
111 | y-component of Cartesian coordinates
112 | z : float or array_like
113 | z-component of Cartesian coordinates
114 |
115 | Returns
116 | -------
117 | alpha : float or `numpy.ndarray`
118 | Azimuth angle in radiants
119 | beta : float or `numpy.ndarray`
120 | Colatitude angle in radiants (with 0 denoting North pole)
121 | r : float or `numpy.ndarray`
122 | Radius
123 |
124 | """
125 | r = np.sqrt(x**2 + y**2 + z**2)
126 | alpha = np.arctan2(y, x)
127 | beta = np.arccos(z / r)
128 | return alpha, beta, r
129 |
130 |
131 | def asarray_1d(a, **kwargs):
132 | """Squeeze the input and check if the result is one-dimensional.
133 |
134 | Returns *a* converted to a `numpy.ndarray` and stripped of
135 | all singleton dimensions. Scalars are "upgraded" to 1D arrays.
136 | The result must have exactly one dimension.
137 | If not, an error is raised.
138 |
139 | """
140 | result = np.squeeze(np.asarray(a, **kwargs))
141 | if result.ndim == 0:
142 | result = result.reshape((1,))
143 | elif result.ndim > 1:
144 | raise ValueError("array must be one-dimensional")
145 | return result
146 |
147 |
148 | def asarray_of_rows(a, **kwargs):
149 | """Convert to 2D array, turn column vector into row vector.
150 |
151 | Returns *a* converted to a `numpy.ndarray` and stripped of
152 | all singleton dimensions. If the result has exactly one dimension,
153 | it is re-shaped into a 2D row vector.
154 |
155 | """
156 | result = np.squeeze(np.asarray(a, **kwargs))
157 | if result.ndim == 1:
158 | result = result.reshape(1, -1)
159 | return result
160 |
161 |
162 | def as_xyz_components(components, **kwargs):
163 | r"""Convert *components* to `XyzComponents` of `numpy.ndarray`\s.
164 |
165 | The *components* are first converted to NumPy arrays (using
166 | :func:`numpy.asarray`) which are then assembled into an
167 | `XyzComponents` object.
168 |
169 | Parameters
170 | ----------
171 | components : triple or pair of array_like
172 | The values to be used as X, Y and Z arrays. Z is optional.
173 | **kwargs
174 | All further arguments are forwarded to :func:`numpy.asarray`,
175 | which is applied to the elements of *components*.
176 |
177 | """
178 | return XyzComponents([np.asarray(c, **kwargs) for c in components])
179 |
180 |
181 | def as_delayed_signal(arg, **kwargs):
182 | """Make sure that the given argument can be used as a signal.
183 |
184 | Parameters
185 | ----------
186 | arg : sequence of 1 array_like followed by 1 or 2 scalars
187 | The first element is converted to a NumPy array, the second
188 | element is used as the sampling rate (in Hertz) and the optional
189 | third element is used as the starting time of the signal (in
190 | seconds). Default starting time is 0.
191 | **kwargs
192 | All keyword arguments are forwarded to :func:`numpy.asarray`.
193 |
194 | Returns
195 | -------
196 | `DelayedSignal`
197 | A named tuple consisting of a `numpy.ndarray` containing the
198 | audio data, followed by the sampling rate (in Hertz) and the
199 | starting time (in seconds) of the signal.
200 |
201 | Examples
202 | --------
203 | Typically, this is used together with tuple unpacking to assign the
204 | audio data, the sampling rate and the starting time to separate
205 | variables:
206 |
207 | >>> import sfs
208 | >>> sig = [1], 44100
209 | >>> data, fs, signal_offset = sfs.util.as_delayed_signal(sig)
210 | >>> data
211 | array([1])
212 | >>> fs
213 | 44100
214 | >>> signal_offset
215 | 0
216 |
217 | """
218 | try:
219 | data, samplerate, *time = arg
220 | time, = time or [0]
221 | except (IndexError, TypeError, ValueError):
222 | pass
223 | else:
224 | valid_arguments = (not np.isscalar(data) and
225 | np.isscalar(samplerate) and
226 | np.isscalar(time))
227 | if valid_arguments:
228 | data = np.asarray(data, **kwargs)
229 | return DelayedSignal(data, samplerate, time)
230 | raise TypeError('expected audio data, samplerate, optional start time')
231 |
232 |
233 | def strict_arange(start, stop, step=1, *, endpoint=False, dtype=None,
234 | **kwargs):
235 | """Like :func:`numpy.arange`, but compensating numeric errors.
236 |
237 | Unlike :func:`numpy.arange`, but similar to :func:`numpy.linspace`,
238 | providing ``endpoint=True`` includes both endpoints.
239 |
240 | Parameters
241 | ----------
242 | start, stop, step, dtype
243 | See :func:`numpy.arange`.
244 | endpoint
245 | See :func:`numpy.linspace`.
246 |
247 | .. note:: With ``endpoint=True``, the difference between *start*
248 | and *end* value must be an integer multiple of the
249 | corresponding *spacing* value!
250 | **kwargs
251 | All further arguments are forwarded to :func:`numpy.isclose`.
252 |
253 | Returns
254 | -------
255 | `numpy.ndarray`
256 | Array of evenly spaced values. See :func:`numpy.arange`.
257 |
258 | """
259 | remainder = (stop - start) % step
260 | if np.any(np.isclose(remainder, (0.0, step), **kwargs)):
261 | if endpoint:
262 | stop += step * 0.5
263 | else:
264 | stop -= step * 0.5
265 | elif endpoint:
266 | raise ValueError("Invalid stop value for endpoint=True")
267 | return np.arange(start, stop, step, dtype)
268 |
269 |
270 | def xyz_grid(x, y, z, *, spacing, endpoint=True, **kwargs):
271 | """Create a grid with given range and spacing.
272 |
273 | Parameters
274 | ----------
275 | x, y, z : float or pair of float
276 | Inclusive range of the respective coordinate or a single value
277 | if only a slice along this dimension is needed.
278 | spacing : float or triple of float
279 | Grid spacing. If a single value is specified, it is used for
280 | all dimensions, if multiple values are given, one value is used
281 | per dimension. If a dimension (*x*, *y* or *z*) has only a
282 | single value, the corresponding spacing is ignored.
283 | endpoint : bool, optional
284 | If ``True`` (the default), the endpoint of each range is
285 | included in the grid. Use ``False`` to get a result similar to
286 | :func:`numpy.arange`. See `strict_arange()`.
287 | **kwargs
288 | All further arguments are forwarded to `strict_arange()`.
289 |
290 | Returns
291 | -------
292 | `XyzComponents`
293 | A grid that can be used for sound field calculations.
294 |
295 | See Also
296 | --------
297 | strict_arange, numpy.meshgrid
298 |
299 | """
300 | if np.isscalar(spacing):
301 | spacing = [spacing] * 3
302 | ranges = []
303 | scalars = []
304 | for i, coord in enumerate([x, y, z]):
305 | if np.isscalar(coord):
306 | scalars.append((i, coord))
307 | else:
308 | start, stop = coord
309 | ranges.append(strict_arange(start, stop, spacing[i],
310 | endpoint=endpoint, **kwargs))
311 | grid = np.meshgrid(*ranges, sparse=True, copy=False)
312 | for i, s in scalars:
313 | grid.insert(i, s)
314 | return XyzComponents(grid)
315 |
316 |
317 | def normalize(p, grid, xnorm):
318 | """Normalize sound field wrt position *xnorm*."""
319 | return p / np.abs(probe(p, grid, xnorm))
320 |
321 |
322 | def probe(p, grid, x):
323 | """Determine the value at position *x* in the sound field *p*."""
324 | grid = as_xyz_components(grid)
325 | x = asarray_1d(x)
326 | r = np.linalg.norm(grid - x)
327 | idx = np.unravel_index(r.argmin(), r.shape)
328 | return p[idx]
329 |
330 |
331 | def broadcast_zip(*args):
332 | """Broadcast arguments to the same shape and then use :func:`zip`."""
333 | return zip(*np.broadcast_arrays(*args))
334 |
335 |
336 | def normalize_vector(x):
337 | """Normalize a 1D vector."""
338 | x = asarray_1d(x)
339 | return x / np.linalg.norm(x)
340 |
341 |
342 | def db(x, *, power=False):
343 | """Convert *x* to decibel.
344 |
345 | Parameters
346 | ----------
347 | x : array_like
348 | Input data. Values of 0 lead to negative infinity.
349 | power : bool, optional
350 | If ``power=False`` (the default), *x* is squared before
351 | conversion.
352 |
353 | """
354 | with np.errstate(divide='ignore'):
355 | return (10 if power else 20) * np.log10(np.abs(x))
356 |
357 |
358 | class XyzComponents(np.ndarray):
359 | """See __init__()."""
360 |
361 | def __init__(self, components):
362 | r"""Triple (or pair) of components: x, y, and optionally z.
363 |
364 | Instances of this class can be used to store coordinate grids
365 | (either regular grids like in `xyz_grid()` or arbitrary point
366 | clouds) or vector fields (e.g. particle velocity).
367 |
368 | This class is a subclass of `numpy.ndarray`. It is
369 | one-dimensional (like a plain `list`) and has a length of 3 (or
370 | 2, if no z-component is available). It uses ``dtype=object`` in
371 | order to be able to store other `numpy.ndarray`\s of arbitrary
372 | shapes but also scalars, if needed. Because it is a NumPy array
373 | subclass, it can be used in operations with scalars and "normal"
374 | NumPy arrays, as long as they have a compatible shape. Like any
375 | NumPy array, instances of this class are iterable and can be
376 | used, e.g., in for-loops and tuple unpacking. If slicing or
377 | broadcasting leads to an incompatible shape, a plain
378 | `numpy.ndarray` with ``dtype=object`` is returned.
379 |
380 | To make sure the *components* are NumPy arrays themselves, use
381 | `as_xyz_components()`.
382 |
383 | Parameters
384 | ----------
385 | components : (3,) or (2,) array_like
386 | The values to be used as X, Y and Z data. Z is optional.
387 |
388 | """
389 | # This method does nothing, it's only here for the documentation!
390 |
391 | def __new__(cls, components):
392 | # object arrays cannot be created and populated in a single step:
393 | obj = np.ndarray.__new__(cls, len(components), dtype=object)
394 | for i, component in enumerate(components):
395 | obj[i] = component
396 | return obj
397 |
398 | def __array_finalize__(self, obj):
399 | if self.ndim == 0:
400 | pass # this is allowed, e.g. for np.inner()
401 | elif self.ndim > 1 or len(self) not in (2, 3):
402 | raise ValueError("XyzComponents can only have 2 or 3 components")
403 |
404 | def __array_prepare__(self, obj, context=None):
405 | if obj.ndim == 1 and len(obj) in (2, 3):
406 | return obj.view(XyzComponents)
407 | return obj
408 |
409 | def __array_wrap__(self, obj, context=None):
410 | if obj.ndim != 1 or len(obj) not in (2, 3):
411 | return obj.view(np.ndarray)
412 | return obj
413 |
414 | def __getitem__(self, index):
415 | if isinstance(index, slice):
416 | start, stop, step = index.indices(len(self))
417 | if start == 0 and stop in (2, 3) and step == 1:
418 | return np.ndarray.__getitem__(self, index)
419 | # Slices other than xy and xyz are "downgraded" to ndarray
420 | return np.ndarray.__getitem__(self.view(np.ndarray), index)
421 |
422 | def __repr__(self):
423 | return 'XyzComponents(\n' + ',\n'.join(
424 | ' {}={}'.format(name, repr(data).replace('\n', '\n '))
425 | for name, data in zip('xyz', self)) + ')'
426 |
427 | def make_property(index, doc):
428 |
429 | def getter(self):
430 | return self[index]
431 |
432 | def setter(self, value):
433 | self[index] = value
434 |
435 | return property(getter, setter, doc=doc)
436 |
437 | x = make_property(0, doc='x-component.')
438 | y = make_property(1, doc='y-component.')
439 | z = make_property(2, doc='z-component (optional).')
440 |
441 | del make_property
442 |
443 | def apply(self, func, *args, **kwargs):
444 | """Apply a function to each component.
445 |
446 | The function *func* will be called once for each component,
447 | passing the current component as first argument. All further
448 | arguments are passed after that.
449 | The results are returned as a new `XyzComponents` object.
450 |
451 | """
452 | return XyzComponents([func(i, *args, **kwargs) for i in self])
453 |
454 |
455 | DelayedSignal = collections.namedtuple('DelayedSignal', 'data samplerate time')
456 | """A tuple of audio data, sampling rate and start time.
457 |
458 | This class (a `collections.namedtuple`) is not meant to be instantiated
459 | by users.
460 |
461 | To pass a signal to a function, just use a simple `tuple` or `list`
462 | containing the audio data and the sampling rate (in Hertz), with an
463 | optional starting time (in seconds) as a third item.
464 | If you want to ensure that a given variable contains a valid signal, use
465 | `sfs.util.as_delayed_signal()`.
466 |
467 | """
468 |
469 |
470 | def image_sources_for_box(x, L, N, *, prune=True):
471 | """Image source method for a cuboid room.
472 |
473 | The classical method by Allen and Berkley :cite:`Allen1979`.
474 |
475 | Parameters
476 | ----------
477 | x : (D,) array_like
478 | Original source location within box.
479 | Values between 0 and corresponding side length.
480 | L : (D,) array_like
481 | side lengths of room.
482 | N : int
483 | Maximum number of reflections per image source, see below.
484 | prune : bool, optional
485 | selection of image sources:
486 |
487 | - If True (default):
488 | Returns all images reflected up to N times.
489 | This is the usual interpretation of N as "maximum order".
490 |
491 | - If False:
492 | Returns reflected up to N times between individual wall pairs,
493 | a total number of :math:`M := (2N+1)^D`.
494 | This larger set is useful e.g. to select image sources based on
495 | distance to listener, as suggested by :cite:`Borish1984`.
496 |
497 |
498 | Returns
499 | -------
500 | xs : (M, D) `numpy.ndarray`
501 | original & image source locations.
502 | wall_count : (M, 2D) `numpy.ndarray`
503 | number of reflections at individual walls for each source.
504 |
505 | """
506 | def _images_1d_unit_box(x, N):
507 | result = np.arange(-N, N + 1, dtype=x.dtype)
508 | result[N % 2::2] += x
509 | result[1 - (N % 2)::2] += 1 - x
510 | return result
511 |
512 | def _count_walls_1d(a):
513 | b = np.floor(a/2)
514 | c = np.ceil((a-1)/2)
515 | return np.abs(np.stack([b, c], axis=1)).astype(int)
516 |
517 | L = asarray_1d(L)
518 | x = asarray_1d(x)/L
519 | D = len(x)
520 | xs = [_images_1d_unit_box(coord, N) for coord in x]
521 | xs = np.reshape(np.transpose(np.meshgrid(*xs, indexing='ij')), (-1, D))
522 |
523 | wall_count = np.concatenate([_count_walls_1d(d) for d in xs.T], axis=1)
524 | xs *= L
525 |
526 | if prune is True:
527 | N_mask = np.sum(wall_count, axis=1) <= N
528 | xs = xs[N_mask, :]
529 | wall_count = wall_count[N_mask, :]
530 |
531 | return xs, wall_count
532 |
533 |
534 | def spherical_hn2(n, z):
535 | r"""Spherical Hankel function of 2nd kind.
536 |
537 | Defined as https://dlmf.nist.gov/10.47.E6,
538 |
539 | .. math::
540 |
541 | \hankel{2}{n}{z} = \sqrt{\frac{\pi}{2z}}
542 | \Hankel{2}{n + \frac{1}{2}}{z},
543 |
544 | where :math:`\Hankel{2}{n}{\cdot}` is the Hankel function of the
545 | second kind and n-th order, and :math:`z` its complex argument.
546 |
547 | Parameters
548 | ----------
549 | n : array_like
550 | Order of the spherical Hankel function (n >= 0).
551 | z : array_like
552 | Argument of the spherical Hankel function.
553 |
554 | """
555 | return spherical_jn(n, z) - 1j * spherical_yn(n, z)
556 |
557 |
558 | def source_selection_plane(n0, n):
559 | """Secondary source selection for a plane wave.
560 |
561 | Eq.(13) from :cite:`Spors2008`
562 |
563 | """
564 | n0 = asarray_of_rows(n0)
565 | n = normalize_vector(n)
566 | return np.inner(n, n0) >= default.selection_tolerance
567 |
568 |
569 | def source_selection_point(n0, x0, xs):
570 | """Secondary source selection for a point source.
571 |
572 | Eq.(15) from :cite:`Spors2008`
573 |
574 | """
575 | n0 = asarray_of_rows(n0)
576 | x0 = asarray_of_rows(x0)
577 | xs = asarray_1d(xs)
578 | ds = x0 - xs
579 | return inner1d(ds, n0) >= default.selection_tolerance
580 |
581 |
582 | def source_selection_line(n0, x0, xs):
583 | """Secondary source selection for a line source.
584 |
585 | compare Eq.(15) from :cite:`Spors2008`
586 |
587 | """
588 | return source_selection_point(n0, x0, xs)
589 |
590 |
591 | def source_selection_focused(ns, x0, xs):
592 | """Secondary source selection for a focused source.
593 |
594 | Eq.(2.78) from :cite:`Wierstorf2014`
595 |
596 | """
597 | x0 = asarray_of_rows(x0)
598 | xs = asarray_1d(xs)
599 | ns = normalize_vector(ns)
600 | ds = xs - x0
601 | return inner1d(ns, ds) >= default.selection_tolerance
602 |
603 |
604 | def source_selection_all(N):
605 | """Select all secondary sources."""
606 | return np.ones(N, dtype=bool)
607 |
608 |
609 | def max_order_circular_harmonics(N):
610 | r"""Maximum order of 2D/2.5D HOA.
611 |
612 | It returns the maximum order for which no spatial aliasing appears.
613 | It is given on page 132 of :cite:`Ahrens2012` as
614 |
615 | .. math::
616 | \mathtt{max\_order} =
617 | \begin{cases}
618 | N/2 - 1 & \text{even}\;N \\
619 | (N-1)/2 & \text{odd}\;N,
620 | \end{cases}
621 |
622 | which is equivalent to
623 |
624 | .. math::
625 | \mathtt{max\_order} = \big\lfloor \frac{N - 1}{2} \big\rfloor.
626 |
627 | Parameters
628 | ----------
629 | N : int
630 | Number of secondary sources.
631 |
632 | """
633 | return (N - 1) // 2
634 |
635 |
636 | def max_order_spherical_harmonics(N):
637 | r"""Maximum order of 3D HOA.
638 |
639 | .. math::
640 | \mathtt{max\_order} = \lfloor \sqrt{N} \rfloor - 1.
641 |
642 | Parameters
643 | ----------
644 | N : int
645 | Number of secondary sources.
646 |
647 | """
648 | return int(np.sqrt(N) - 1)
649 |
--------------------------------------------------------------------------------
/tests/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest
2 |
--------------------------------------------------------------------------------
/tests/test_array.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from numpy.testing import assert_array_equal
3 | import pytest
4 | import sfs
5 |
6 |
7 | def vectortypes(*coeffs):
8 | return [
9 | list(coeffs),
10 | tuple(coeffs),
11 | np.array(coeffs),
12 | np.array(coeffs).reshape(1, -1),
13 | np.array(coeffs).reshape(-1, 1),
14 | ]
15 |
16 |
17 | def vector_id(vector):
18 | if isinstance(vector, np.ndarray):
19 | return 'array, shape=' + repr(vector.shape)
20 | return type(vector).__name__
21 |
22 |
23 | @pytest.mark.parametrize('N, spacing, result', [
24 | (2, 1, sfs.array.SecondarySourceDistribution(
25 | x=[[0, -0.5, 0], [0, 0.5, 0]],
26 | n=[[1, 0, 0], [1, 0, 0]],
27 | a=[1, 1],
28 | )),
29 | (3, 1, sfs.array.SecondarySourceDistribution(
30 | x=[[0, -1, 0], [0, 0, 0], [0, 1, 0]],
31 | n=[[1, 0, 0], [1, 0, 0], [1, 0, 0]],
32 | a=[1, 1, 1],
33 | )),
34 | (3, 0.5, sfs.array.SecondarySourceDistribution(
35 | x=[[0, -0.5, 0], [0, 0, 0], [0, 0.5, 0]],
36 | n=[[1, 0, 0], [1, 0, 0], [1, 0, 0]],
37 | a=[0.5, 0.5, 0.5],
38 | )),
39 | ])
40 | def test_linear_with_defaults(N, spacing, result):
41 | a = sfs.array.linear(N, spacing)
42 | assert a.x.dtype == np.float64
43 | assert a.n.dtype == np.float64
44 | assert a.a.dtype == np.float64
45 | assert_array_equal(a.x, result.x)
46 | assert_array_equal(a.n, result.n)
47 | assert_array_equal(a.a, result.a)
48 |
49 |
50 | def test_linear_with_named_arguments():
51 | a = sfs.array.linear(N=2, spacing=0.5)
52 | assert_array_equal(a.x, [[0, -0.25, 0], [0, 0.25, 0]])
53 | assert_array_equal(a.n, [[1, 0, 0], [1, 0, 0]])
54 | assert_array_equal(a.a, [0.5, 0.5])
55 |
56 |
57 | @pytest.mark.parametrize('center', vectortypes(-1, 0.5, 2), ids=vector_id)
58 | def test_linear_with_center(center):
59 | a = sfs.array.linear(2, 1, center=center)
60 | assert_array_equal(a.x, [[-1, 0, 2], [-1, 1, 2]])
61 | assert_array_equal(a.n, [[1, 0, 0], [1, 0, 0]])
62 | assert_array_equal(a.a, [1, 1])
63 |
64 |
65 | @pytest.mark.parametrize('orientation', vectortypes(0, -1, 0), ids=vector_id)
66 | def test_linear_with_center_and_orientation(orientation):
67 | a = sfs.array.linear(2, 1, center=[0, 1, 2], orientation=orientation)
68 | assert_array_equal(a.x, [[-0.5, 1, 2], [0.5, 1, 2]])
69 |
--------------------------------------------------------------------------------
/tests/test_util.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from numpy.testing import assert_allclose
3 | import pytest
4 | import sfs
5 |
6 |
7 | cart_sph_data = [
8 | ((1, 1, 1), (np.pi / 4, np.arccos(1 / np.sqrt(3)), np.sqrt(3))),
9 | ((-1, 1, 1), (3 / 4 * np.pi, np.arccos(1 / np.sqrt(3)), np.sqrt(3))),
10 | ((1, -1, 1), (-np.pi / 4, np.arccos(1 / np.sqrt(3)), np.sqrt(3))),
11 | ((-1, -1, 1), (-3 / 4 * np.pi, np.arccos(1 / np.sqrt(3)), np.sqrt(3))),
12 | ((1, 1, -1), (np.pi / 4, np.arccos(-1 / np.sqrt(3)), np.sqrt(3))),
13 | ((-1, 1, -1), (3 / 4 * np.pi, np.arccos(-1 / np.sqrt(3)), np.sqrt(3))),
14 | ((1, -1, -1), (-np.pi / 4, np.arccos(-1 / np.sqrt(3)), np.sqrt(3))),
15 | ((-1, -1, -1), (-3 / 4 * np.pi, np.arccos(-1 / np.sqrt(3)), np.sqrt(3))),
16 | ]
17 |
18 |
19 | @pytest.mark.parametrize('coord, polar', cart_sph_data)
20 | def test_cart2sph(coord, polar):
21 | x, y, z = coord
22 | a = sfs.util.cart2sph(x, y, z)
23 | assert_allclose(a, polar)
24 |
25 |
26 | @pytest.mark.parametrize('coord, polar', cart_sph_data)
27 | def test_sph2cart(coord, polar):
28 | alpha, beta, r = polar
29 | b = sfs.util.sph2cart(alpha, beta, r)
30 | assert_allclose(b, coord)
31 |
32 |
33 | direction_vector_data = [
34 | ((np.pi / 4, np.pi / 4), (0.5, 0.5, np.sqrt(2) / 2)),
35 | ((3 * np.pi / 4, 3 * np.pi / 4), (-1 / 2, 1 / 2, -np.sqrt(2) / 2)),
36 | ((3 * np.pi / 4, -3 * np.pi / 4), (1 / 2, -1 / 2, -np.sqrt(2) / 2)),
37 | ((np.pi / 4, -np.pi / 4), (-1 / 2, -1 / 2, np.sqrt(2) / 2)),
38 | ((-np.pi / 4, np.pi / 4), (1 / 2, -1 / 2, np.sqrt(2) / 2)),
39 | ((-3 * np.pi / 4, 3 * np.pi / 4), (-1 / 2, -1 / 2, -np.sqrt(2) / 2)),
40 | ((-3 * np.pi / 4, -3 * np.pi / 4), (1 / 2, 1 / 2, -np.sqrt(2) / 2)),
41 | ((-np.pi / 4, -np.pi / 4), (-1 / 2, 1 / 2, np.sqrt(2) / 2)),
42 | ]
43 |
44 |
45 | @pytest.mark.parametrize('input, vector', direction_vector_data)
46 | def test_direction_vector(input, vector):
47 | alpha, beta = input
48 | c = sfs.util.direction_vector(alpha, beta)
49 | assert_allclose(c, vector)
50 |
51 |
52 | db_data = [
53 | (0, -np.inf),
54 | (0.5, -3.01029995663981),
55 | (1, 0),
56 | (2, 3.01029995663981),
57 | (10, 10),
58 | ]
59 |
60 |
61 | @pytest.mark.parametrize('linear, power_db', db_data)
62 | def test_db_amplitude(linear, power_db):
63 | d = sfs.util.db(linear)
64 | assert_allclose(d, power_db * 2)
65 |
66 |
67 | @pytest.mark.parametrize('linear, power_db', db_data)
68 | def test_db_power(linear, power_db):
69 | d = sfs.util.db(linear, power=True)
70 | assert_allclose(d, power_db)
71 |
--------------------------------------------------------------------------------