Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

diffusion coefficient #11

Closed
wants to merge 50 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
2616155
add spherical integration over theta and phi
ahms5 Sep 21, 2022
5381d3b
Apply suggestions from code review
ahms5 Sep 22, 2022
c0ddd18
apply suggestions from @mberz and renew tests
ahms5 Sep 22, 2022
9663447
flake8 fixes and test improvements
ahms5 Sep 22, 2022
647e2bb
add example and value name tests
ahms5 Sep 22, 2022
3c2584d
debug doc
ahms5 Sep 22, 2022
e0e4aa1
flake8
ahms5 Sep 22, 2022
e83d947
draft scattering freefield
ahms5 Sep 22, 2022
3bb2252
fix docs and bugs in scattering.freefield
ahms5 Sep 22, 2022
8cb5b1a
flake8
ahms5 Sep 22, 2022
12344e9
separate calc random_incidence from freefield
ahms5 Sep 23, 2022
92fd361
clean up tests
ahms5 Sep 23, 2022
bb27c7a
cosmetics flake8
ahms5 Sep 23, 2022
8886b4e
flake8
ahms5 Sep 23, 2022
575de91
bugfix for different arragements of angles in coords
ahms5 Nov 29, 2022
2880a59
include review
ahms5 Nov 30, 2022
d351258
include review
ahms5 Nov 30, 2022
d8b9bde
Merge branch 'feature/integrate-spherical' into scattering-freefield
ahms5 Nov 30, 2022
d8812d4
merge scattering-freefield into integrate
ahms5 Nov 30, 2022
6ab3171
flake 8
ahms5 Nov 30, 2022
1546bc2
flake8
ahms5 Nov 30, 2022
0c4ea42
Merge branch 'feature/integrate-spherical' into scattering-freefield
ahms5 Nov 30, 2022
37dab64
flake 8
ahms5 Nov 30, 2022
7ee83f0
Merge branch 'feature/integrate-spherical' into scattering-freefield
ahms5 Nov 30, 2022
56a7970
Apply suggestions from code review
ahms5 Nov 30, 2022
6e020a8
include review for scattering freefield
ahms5 Nov 30, 2022
a1fc6db
fix random scattering
ahms5 Nov 30, 2022
00bd4e8
fix init
ahms5 Nov 30, 2022
9874c81
fix flake8
ahms5 Nov 30, 2022
651880e
Merge branch 'feature/integrate-spherical' into scattering-freefield
ahms5 Nov 30, 2022
73825ae
fix flake8
ahms5 Nov 30, 2022
d4ff85e
added test and rename sub_utils
ahms5 Nov 30, 2022
344ab7d
Merge branch 'feature/integrate-spherical' into scattering-freefield
ahms5 Nov 30, 2022
42193aa
add test for non uniform surface
ahms5 Nov 30, 2022
345c795
Merge branch 'feature/integrate-spherical' into scattering-freefield
ahms5 Nov 30, 2022
11d019a
remove duplicated test
ahms5 Nov 30, 2022
0e6205f
Merge branch 'feature/integrate-spherical' into scattering-freefield
ahms5 Nov 30, 2022
b5d4a9a
fix flake8
ahms5 Nov 30, 2022
dcdb264
Merge branch 'develop' into feature/integrate-spherical
ahms5 Dec 12, 2022
f58168a
Merge branch 'feature/integrate-spherical' into scattering-freefield
ahms5 Dec 12, 2022
fe1337e
add shapes and f of pressure need to be same
ahms5 Jan 24, 2023
b3fe3bd
fix documentation
ahms5 Jan 24, 2023
36c28bf
fix flake8
ahms5 Jan 24, 2023
0366f97
init
ahms5 Feb 7, 2023
698f1af
fix doc
ahms5 Feb 7, 2023
c53e36f
remove random incidence
ahms5 Feb 23, 2023
93901db
weights can be manipulated now
ahms5 Feb 23, 2023
c003c45
Merge branch 'feature/integrate-spherical' into scattering-freefield
ahms5 Feb 23, 2023
07242dd
Merge branch 'scattering-freefield' into diffusion
ahms5 Feb 23, 2023
9770ad0
implement and test diffusion calculation
ahms5 Feb 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,20 @@

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'sphinx.ext.napoleon',
'sphinx.ext.autosummary',
'matplotlib.sphinxext.plot_directive',
'sphinx.ext.imgmath',
'autodocsumm']

imgmath_latex_preamble = r'\usepackage{array}'

# show tocs for classes and functions of modules using the autodocsumm
# package
autodoc_default_options = {'autosummary': True}

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
Expand Down Expand Up @@ -157,6 +170,3 @@
'One line description of project.',
'Miscellaneous'),
]



11 changes: 11 additions & 0 deletions docs/modules.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Modules
=======

The following gives detailed information about all pyfar functions sorted
according to their modules.

.. toctree::
:maxdepth: 1

modules/imkar.integrate
modules/imkar.scattering.coefficient
7 changes: 7 additions & 0 deletions docs/modules/imkar.integrate.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
imkar.integrate
===============

.. automodule:: imkar.integrate
:members:
:undoc-members:
:show-inheritance:
7 changes: 7 additions & 0 deletions docs/modules/imkar.scattering.coefficient.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
imkar.scattering.coefficient
============================

.. automodule:: imkar.scattering.coefficient
:members:
:undoc-members:
:show-inheritance:
5 changes: 5 additions & 0 deletions imkar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
__author__ = """The pyfar developers"""
__email__ = '[email protected]'
__version__ = '0.1.0'


from . import integrate # noqa
from . import scattering # noqa
from . import diffusion # noqa
7 changes: 7 additions & 0 deletions imkar/diffusion/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .coefficient import (
freefield
)


__all__ = [
'freefield']
86 changes: 86 additions & 0 deletions imkar/diffusion/coefficient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import numpy as np
import pyfar as pf


def freefield(sample_pressure, mic_positions):
"""
Calculate the free-field diffusion coefficient for each incident direction
after ISO 17497-2:2012 [1]_. See :py:func:`random_incidence`
to calculate the random incidence diffusion coefficient.


Parameters
----------
sample_pressure : FrequencyData
Reflected sound pressure or directivity of the test sample. Its cshape
need to be (..., #theta_incident_positions, #phi_incident_positions,
#angle1, #angle2)
mic_positions : Coordinates
A Coordinate object with all microphone positions. Its cshape need to
be (#angle1, #angle2). In sperical coordinates the radii need to
be constant. It need to be same for `sample_pressure`.

Returns
-------
diffusion_coefficients : FrequencyData
The diffusion coefficient for each plane wave direction.


References
----------
.. [1] ISO 17497-2:2012, Sound-scattering properties of surfaces.
Part 2: Measurement of the directional diffusion coefficient in a
free field. Geneva, Switzerland: International Organization for
Standards, 2012.


Examples
--------
Calculate free-field diffusion coefficients and then the random incidence
diffusion coefficient.

>>> import imkar as ik
>>> diffusion_coefficients = ik.diffusion.coefficient.freefield(
>>> sample_pressure, mic_positions)
>>> random_d = ik.scattering.coefficient.random_incidence(
>>> diffusion_coefficients, incident_positions)
"""
# check inputs
if not isinstance(sample_pressure, pf.FrequencyData):
raise ValueError("sample_pressure has to be FrequencyData")
if not isinstance(mic_positions, pf.Coordinates):
raise ValueError("microphone positions have to be Coordinates")

# parse angles
coords_spherical = mic_positions.get_sph(convention='top_colat')
phi = coords_spherical[1, :, 0]
theta = coords_spherical[:, 1, 1]
if np.sum(np.diff(phi[1:-2])) < 1e-3:
phi = coords_spherical[:, 1, 0]
theta = coords_spherical[1, :, 1]
d_theta = np.mean(np.diff(theta))
d_phi = np.mean(np.diff(phi))

# calculate weights
thetas = coords_spherical[..., 1]
A_i = 2 * np.sin(thetas) * np.sin(d_theta/2)
A_i[thetas == 0] = 2 * np.sin(d_theta/2)
A_i[thetas == np.pi/2] = 4 * np.pi / d_phi * (np.sin(d_theta/4)**2)
N_i = A_i / np.min(A_i)

# calculate according to Mommertz correlation method Equation (6)
p_sample_abs_sq = np.moveaxis(np.abs(sample_pressure.freq)**2, -1, 0)

p_sample_sum_sq = np.sum(np.sum(
p_sample_abs_sq**2 * N_i, axis=-1), axis=-1)
p_sample_sq_sum = np.sum(np.sum(
p_sample_abs_sq * N_i, axis=-1), axis=-1)**2
n = np.sum(N_i)
diffusion_array \
= (p_sample_sq_sum - p_sample_sum_sq) / ((n-1) * p_sample_sum_sq)
diffusion_coefficients = pf.FrequencyData(
np.moveaxis(diffusion_array, 0, -1),
sample_pressure.frequencies)
diffusion_coefficients.comment = 'diffusion coefficients'

return diffusion_coefficients
1 change: 0 additions & 1 deletion imkar/imkar.py

This file was deleted.

7 changes: 7 additions & 0 deletions imkar/integrate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .integrate import (
surface_sphere
)


__all__ = [
'surface_sphere']
83 changes: 83 additions & 0 deletions imkar/integrate/integrate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import warnings
from scipy.integrate import trapezoid
import numpy as np
import pyfar as pf


def surface_sphere(data, coords, weights=None):
r"""Integrate over a set of points sampled on a spherical surface.

.. math::

S = \int \int data(\phi, \theta) sin(\theta) d\phi d\theta

Parameters
----------
data : FrequencyData, Signal
Input data to integrate. Its `cshape` needs to be (..., #theta, #phi)
or (..., #phi, #theta).
coords : Coordinates
Coordinate points at which the data is sampled. Its `cshape` needs to
be (#theta, #phi) or (#phi, #theta), matching the `cshape` of `data`.
weights : np.array
weights for the integration over the coordinates. Its `cshape`
needs to be same as for `coords`.
By default the its sin(theta).

Returns
-------
FrequencyData
The integration result. Its dimension is reduced by the last two,
which are consumed by the integration.

Examples
--------
>>> from imkar import integrate
>>> from pyfar import FrequencyData, Coordinates
>>> import numpy as np
>>> phi, theta = np.meshgrid(
>>> np.linspace(0, 2*np.pi, num=361),
>>> np.linspace(0, np.pi, num=181))
>>> coords = Coordinates(
>>> phi, theta, np.ones(phi.shape), 'sph')
>>> data_raw = np.ones(phi.shape)
>>> data = FrequencyData(data_raw[..., None], 100)
>>> result = integrate.surface_sphere(data, coords)
>>> result.freq
array([[12.56605162+0.j]])
"""

if coords.cshape != data.cshape[-2:]:
raise ValueError(
f'Coordinates.cshape should be same as {data.cshape[-2:]}')

# parse angles
coords_spherical = coords.get_sph(convention='top_colat')
phi = coords_spherical[1, :, 0]
theta = coords_spherical[:, 1, 1]
axis_index = -1
if np.sum(np.diff(phi[1:-2])) < 1e-3:
phi = coords_spherical[:, 1, 0]
theta = coords_spherical[1, :, 1]
axis_index = -2
radius = coords_spherical[:, :, 2]
r_mean = np.mean(radius)
if not np.allclose(radius, r_mean):
warnings.warn(r'all radii should be almost same')

# pf.Coordinates turns phi = 2*pi to 0, due to circular
# for the integration the upper limit cannot be zero but 2pi
last_phi_is_zero = np.isclose(phi[-1], 0, atol=1e-16)
increasing_phi_to_2pi = np.isclose(
phi[-2]+np.diff(phi)[0], 2*np.pi, atol=1e-16)
if last_phi_is_zero and increasing_phi_to_2pi:
phi[-1] = 2 * np.pi

# integrate over angles with wight for spherical integration
data_int = np.moveaxis(data.freq, -1, 0)
if weights is None:
weights = np.sin(coords_spherical[..., 1])
result_raw = trapezoid(data_int*weights, x=phi, axis=axis_index)
result_raw1 = trapezoid(result_raw, x=theta, axis=-1)

return pf.FrequencyData(np.moveaxis(result_raw1, 0, -1), data.frequencies)
9 changes: 9 additions & 0 deletions imkar/scattering/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .coefficient import (
random_incidence,
freefield
)

__all__ = [
'freefield',
'random_incidence'
]
126 changes: 126 additions & 0 deletions imkar/scattering/coefficient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import numpy as np
import pyfar as pf
from imkar import integrate


def freefield(sample_pressure, reference_pressure, mic_positions):
"""
Calculate the free-field scattering coefficient for each incident direction
using the Mommertz correlation method [1]_. See :py:func:`random_incidence`
to calculate the random incidence scattering coefficient.


Parameters
----------
sample_pressure : FrequencyData
Reflected sound pressure or directivity of the test sample. Its cshape
need to be (..., #angle1, #angle2), see `mic_positions` for more
detail.
reference_pressure : FrequencyData
Reflected sound pressure or directivity of the test
reference sample. It has the same shape as `sample_pressure`.
mic_positions : Coordinates
A Coordinate object with all microphone positions. Its cshape need to
be (#angle1, #angle2). In sperical coordinates the radii need to
be constant. Microphone positions need to be same for
`sample_pressure` and `reference_pressure`.

Returns
-------
scattering_coefficients : FrequencyData
The scattering coefficient for each plane wave direction.


References
----------
.. [1] E. Mommertz, „Determination of scattering coefficients from the
reflection directivity of architectural surfaces“, Applied
Acoustics, Bd. 60, Nr. 2, S. 201–203, Juni 2000,
doi: 10.1016/S0003-682X(99)00057-2.

Examples
--------
Calculate freefield scattering coefficients and then the random incidence
scattering coefficient.

>>> import imkar
>>> scattering_coefficients = imkar.scattering.coefficient.freefield(
>>> sample_pressure, reference_pressure, mic_positions)
>>> random_s = imkar.scattering.coefficient.random_incidence(
>>> scattering_coefficients, incident_positions)
"""
# check inputs
if not isinstance(sample_pressure, pf.FrequencyData):
raise ValueError("sample_pressure has to be FrequencyData")
if not isinstance(reference_pressure, pf.FrequencyData):
raise ValueError("reference_pressure has to be FrequencyData")
if not isinstance(mic_positions, pf.Coordinates):
raise ValueError("microphone positions have to be Coordinates")
if not sample_pressure.cshape == reference_pressure.cshape:
raise ValueError(
"sample_presure and reference_pressure have to have the "
"same cshape.")
if any(sample_pressure.frequencies != reference_pressure.frequencies):
raise ValueError(
"sample_presure and reference_pressure have to have the "
"same frequencies.")

# calculate according to mommertz correlation method Equation (5)
p_sample_abs = pf.FrequencyData(
np.abs(sample_pressure.freq), sample_pressure.frequencies)
p_reference_abs = pf.FrequencyData(
np.abs(reference_pressure.freq), reference_pressure.frequencies)
p_sample_sq = p_sample_abs*p_sample_abs
p_reference_sq = p_reference_abs*p_reference_abs
p_reference_conj = pf.FrequencyData(
np.conj(reference_pressure.freq), reference_pressure.frequencies)
p_cross = sample_pressure*p_reference_conj

p_sample_sum = integrate.surface_sphere(p_sample_sq, mic_positions)
p_ref_sum = integrate.surface_sphere(p_reference_sq, mic_positions)
p_cross_sum = integrate.surface_sphere(p_cross, mic_positions)
p_cross_sum_abs = pf.FrequencyData(
np.abs(p_cross_sum.freq), p_cross_sum.frequencies)

scattering_coefficients \
= 1 - ((p_cross_sum_abs**2)/(p_sample_sum*p_ref_sum))
scattering_coefficients.comment = 'scattering coefficient'

return scattering_coefficients


def random_incidence(scattering_coefficient, incident_positions):
"""Calculate the random-incidence scattering coefficient
according to Paris formula. Note that the incident directions should be
equally distributed to get a valid result.

Parameters
----------
scattering_coefficient : FrequencyData
The scattering coefficient for each plane wave direction. Its cshape
need to be (..., #angle1, #angle2)
incident_positions : Coordinates
Defines the incidence directions of each `scattering_coefficient` in a
Coordinates object. Its cshape need to be (#angle1, #angle2). In
sperical coordinates the radii need to be constant.

Returns
-------
random_scattering : FrequencyData
The random-incidence scattering coefficient.
"""
if not isinstance(scattering_coefficient, pf.FrequencyData):
raise ValueError("scattering_coefficient has to be FrequencyData")
if (incident_positions is not None) & \
~isinstance(incident_positions, pf.Coordinates):
raise ValueError("incident_positions have to be None or Coordinates")

sph = incident_positions.get_sph()
theta = sph[..., 1]
weight = np.sin(2*theta) # sin(2*theta)
norm = np.sum(weight)
random_scattering = scattering_coefficient*weight/norm
random_scattering.freq = np.sum(random_scattering.freq, axis=-2)
random_scattering.freq = np.sum(random_scattering.freq, axis=-2)
random_scattering.comment = 'random-incidence scattering coefficient'
return random_scattering
5 changes: 5 additions & 0 deletions imkar/testing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Testing sub-package for pyfar."""

from . import (stub_utils)

__all__ = ['stub_utils']
Loading