Skip to content

Commit

Permalink
impl new approach for scattering
Browse files Browse the repository at this point in the history
  • Loading branch information
ahms5 committed Feb 24, 2023
1 parent 00413c0 commit a283387
Show file tree
Hide file tree
Showing 10 changed files with 413 additions and 3 deletions.
2 changes: 1 addition & 1 deletion docs/modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ according to their modules.
.. toctree::
:maxdepth: 1


modules/imkar.scattering.coefficient
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:
3 changes: 3 additions & 0 deletions imkar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
__author__ = """The pyfar developers"""
__email__ = 'info@pyfar.org'
__version__ = '0.1.0'


from . import scattering # noqa
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'
]
127 changes: 127 additions & 0 deletions imkar/scattering/coefficient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import numpy as np
import pyfar as pf


def freefield(sample_pressure, reference_pressure, weights_microphones):
"""
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 : pf.FrequencyData
Reflected sound pressure or directivity of the test sample. Its cshape
need to be (..., #microphones).
reference_pressure : pf.FrequencyData
Reflected sound pressure or directivity of the test
reference sample. It has the same shape as `sample_pressure`.
weights_microphones : np.ndarray
An array object with all weights for the microphone positions.
Its cshape need to be (#microphones). Microphone positions need to be
same for `sample_pressure` and `reference_pressure`.
Returns
-------
scattering_coefficients : FrequencyData
The scattering coefficient for each incident 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, June 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.weights)
>>> 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(weights_microphones, np.ndarray):
raise ValueError("weights_microphones have to be a numpy.array")
if not sample_pressure.cshape == reference_pressure.cshape:
raise ValueError(
"sample_pressure and reference_pressure have to have the "
"same cshape.")
if not weights_microphones.shape[0] == sample_pressure.cshape[-1]:
raise ValueError(
"the last dimension of sample_pressure need be same as the "
"weights_microphones.shape.")
if not np.allclose(
sample_pressure.frequencies, reference_pressure.frequencies):
raise ValueError(
"sample_pressure and reference_pressure have to have the "
"same frequencies.")

# calculate according to mommertz correlation method Equation (5)
p_sample = np.moveaxis(sample_pressure.freq, -1, 0)
p_reference = np.moveaxis(reference_pressure.freq, -1, 0)
p_sample_abs = np.abs(p_sample)
p_reference_abs = np.abs(p_reference)
p_sample_sq = p_sample_abs*p_sample_abs
p_reference_sq = p_reference_abs*p_reference_abs
p_cross = p_sample * np.conj(p_reference)

p_sample_sum = np.sum(p_sample_sq * weights_microphones, axis=-1)
p_ref_sum = np.sum(p_reference_sq * weights_microphones, axis=-1)
p_cross_sum = np.sum(p_cross * weights_microphones, axis=-1)

data_scattering_coefficient \
= 1 - ((np.abs(p_cross_sum)**2)/(p_sample_sum*p_ref_sum))

scattering_coefficients = pf.FrequencyData(
np.moveaxis(data_scattering_coefficient, 0, -1),
sample_pressure.frequencies)
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 : pf.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")

theta = incident_positions.get_sph().T[1]
weight = np.cos(theta) * incident_positions.weights
norm = np.sum(weight)
random_scattering = scattering_coefficient*weight/norm
random_scattering.freq = np.sum(random_scattering.freq, axis=-2)
random_scattering.comment = 'random-incidence scattering coefficient'
return random_scattering
7 changes: 7 additions & 0 deletions imkar/testing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .stub_utils import (
frequency_data_from_shape,
)

__all__ = [
'frequency_data_from_shape',
]
22 changes: 22 additions & 0 deletions imkar/testing/stub_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
Contains tools to easily generate stubs for the most common pyfar Classes.
Stubs are used instead of pyfar objects for testing functions that have pyfar
objects as input arguments. This makes testing such functions independent from
the pyfar objects themselves and helps to find bugs.
"""
import numpy as np
import pyfar as pf


def frequency_data_from_shape(shape, data_raw, frequencies):
frequencies = np.atleast_1d(frequencies)
shape_new = np.append(shape, frequencies.shape)
if hasattr(data_raw, "__len__"): # is array
if len(shape) > 0:
for dim in shape:
data_raw = np.repeat(data_raw[..., np.newaxis], dim, axis=-1)
data = np.repeat(data_raw[..., np.newaxis], len(frequencies), axis=-1)
else:
data = np.zeros(shape_new) + data_raw
p_reference = pf.FrequencyData(data, frequencies)
return p_reference
4 changes: 2 additions & 2 deletions tests/test_imkar.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest


from imkar import imkar
from imkar import imkar # noqa


@pytest.fixture
Expand All @@ -18,7 +18,7 @@ def response():
# return requests.get('https://github.com/mberz/cookiecutter-pypackage')


def test_content(response):
def test_content(response): # noqa
"""Sample pytest test function with the pytest fixture as an argument."""
# from bs4 import BeautifulSoup
# assert 'GitHub' in BeautifulSoup(response.content).title.string
Loading

0 comments on commit a283387

Please sign in to comment.