-
Notifications
You must be signed in to change notification settings - Fork 2
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
New scattering freefield implementation #12
base: develop
Are you sure you want to change the base?
Changes from all commits
a283387
bcb4fbc
38eefd7
a88b34f
5b8a350
ac207bc
800b72c
2227ef4
47802e4
112cf90
0baf823
00f76b1
b29353f
5c7408e
0987a4a
bb33688
d0544c9
87c48bf
ada05f8
df55753
4d892eb
f993b39
02ef6db
8d3030e
e005c98
ac8de6f
08d0b0f
ae4140d
4f98a5b
599cd70
723c48c
ffaad46
08504b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -104,6 +104,3 @@ ENV/ | |
# IDE settings | ||
.vscode/ | ||
.idea/ | ||
|
||
# OS stuff | ||
.DS_Store |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,4 +7,5 @@ according to their modules. | |
.. toctree:: | ||
:maxdepth: 1 | ||
|
||
|
||
modules/imkar.scattering | ||
modules/imkar.utils |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
imkar.scattering | ||
================ | ||
|
||
.. automodule:: imkar.scattering | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
imkar.utils | ||
=========== | ||
|
||
.. automodule:: imkar.utils | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
""" | ||
This module collects the functionality around sound scattering, such as, | ||
(random incident) scattering coefficients, and analytical solutions. | ||
""" | ||
|
||
from .scattering import ( | ||
freefield, | ||
random, | ||
) | ||
|
||
__all__ = [ | ||
'freefield', | ||
'random', | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import numpy as np | ||
import pyfar as pf | ||
Comment on lines
+1
to
+2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See issue - dependencies must be added to |
||
from imkar import utils | ||
|
||
|
||
def freefield(sample_pressure, reference_pressure, microphone_weights): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be stupid me and the long while since I had to deal with computing scattering coefficients, but I'm confused with the following: If I pass 10-channel, 3-frequencies data the function returns single-channel, 3-frequencies data, but the docstring mentions that the scattering is computed for each incidence. Can you make more clear what the angles _S and _R are and how they are encoded in the input and output data? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good point, it does not need to be for each incident angle, because it depends on the input data, what ever is in the dimentions before the last one, gets retured, so I delete the reference for each incident angle. if we use None, we need the data points instead, and it will get more complex, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I ment |
||
r""" | ||
Calculate the direction dependent free-field scattering coefficient. | ||
|
||
Uses the Mommertz correlation method [1]_ to calculate the scattering | ||
coefficient of the input data: | ||
|
||
.. math:: | ||
s = 1 - | ||
\frac{|\sum_w \underline{p}_{\text{sample}}(\vartheta,\varphi) | ||
\cdot \underline{p}_{\text{reference}}^*(\vartheta,\varphi) | ||
\cdot w(\vartheta,\varphi)|^2} | ||
{\sum_w |\underline{p}_{\text{sample}}(\vartheta,\varphi)|^2 | ||
\cdot w(\vartheta,\varphi) \cdot \sum_w | ||
|\underline{p}_{\text{reference}}(\vartheta,\varphi)|^2 | ||
\cdot w(\vartheta,\varphi) } | ||
|
||
with the reflected sound pressure of the the sample under investigation | ||
:math:`\underline{p}_{\text{sample}}`, the reflected sound pressure from | ||
the reference sample (same dimension as the sample under investigation, | ||
but with flat surface) :math:`\underline{p}_{\text{reference}}`, the | ||
area weights of the sampling :math:`w`, and :math:`\vartheta` and | ||
:math:`\varphi` are the ``colatitude`` | ||
angle and ``azimuth`` angles from the | ||
:py:class:`~pyfar.classes.coordinates.Coordinates` object. | ||
In other words, the test sample lies in the x-y-plane. See | ||
:py:func:`random` to calculate the random incidence | ||
scattering coefficient. | ||
|
||
Parameters | ||
---------- | ||
sample_pressure : :py:class:`~pyfar.classes.audio.FrequencyData` | ||
Reflected sound pressure or directivity of the test sample. Its cshape | ||
needs to be (..., microphone_weights.size). | ||
reference_pressure : :py:class:`~pyfar.classes.audio.FrequencyData` | ||
Reflected sound pressure or directivity of the | ||
reference sample. Needs to have the same cshape and frequencies as | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we link to pyfar audio concepts when refering to |
||
`sample_pressure`. | ||
microphone_weights : numpy.ndarray | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are there constraints on the weights that should be mentioned? E.g., should they sum to 1? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need to be normalized or just area weights, but mybe i could mention it |
||
Array containing the area weights for the microphone positions, | ||
no normalization required. | ||
Its shape needs to match the last dimension in the cshape of | ||
`sample_pressure` and `reference_pressure`. | ||
|
||
Returns | ||
------- | ||
scattering_coefficients : :py:class:`~pyfar.classes.audio.FrequencyData` | ||
The scattering coefficient for each incident direction depending on | ||
frequency. | ||
|
||
|
||
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. | ||
|
||
""" | ||
# check inputs | ||
if not isinstance(sample_pressure, pf.FrequencyData): | ||
raise ValueError( | ||
"sample_pressure has to be a pyfar.FrequencyData object") | ||
if not isinstance(reference_pressure, pf.FrequencyData): | ||
raise ValueError( | ||
"reference_pressure has to be a pyfar.FrequencyData object") | ||
microphone_weights = np.atleast_1d( | ||
np.asarray(microphone_weights, dtype=float)) | ||
if sample_pressure.cshape != reference_pressure.cshape: | ||
raise ValueError( | ||
"sample_pressure and reference_pressure have to have the " | ||
"same cshape.") | ||
if microphone_weights.shape[0] != sample_pressure.cshape[-1]: | ||
raise ValueError( | ||
"the last dimension of sample_pressure needs be same as the " | ||
"microphone_weights.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_sq = np.abs(p_sample)**2 | ||
p_reference_sq = np.abs(p_reference)**2 | ||
p_cross = p_sample * np.conj(p_reference) | ||
|
||
p_sample_sum = np.sum(microphone_weights * p_sample_sq, axis=-1) | ||
p_ref_sum = np.sum(microphone_weights * p_reference_sq, axis=-1) | ||
p_cross_sum = np.sum(microphone_weights * p_cross, 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) | ||
|
||
return scattering_coefficients | ||
|
||
|
||
def random( | ||
scattering_coefficients, incident_directions): | ||
r""" | ||
Calculate the random-incidence scattering coefficient from free-field | ||
data for several incident directions. | ||
|
||
Uses the Paris formula [2]_. | ||
|
||
.. math:: | ||
s_{rand} = \sum s(\vartheta,\varphi) \cdot cos(\vartheta) \cdot | ||
w(\vartheta,\varphi) | ||
|
||
with the scattering coefficients :math:`s(\vartheta,\varphi)`, the area | ||
weights ``w`` taken from the `incident_directions.weights`, | ||
and :math:`\vartheta` and :math:`\varphi` are the ``colatitude`` | ||
angle and ``azimuth`` angles from the | ||
:py:class:`~pyfar.classes.coordinates.Coordinates` object. | ||
Note that the incident directions should be | ||
equally distributed to get a valid result. See | ||
:py:func:`freefield` to calculate the free-field scattering coefficient. | ||
|
||
Parameters | ||
---------- | ||
scattering_coefficients : :py:class:`~pyfar.classes.audio.FrequencyData` | ||
Scattering coefficients for different incident directions. Its cshape | ||
needs to be (..., incident_directions.csize) | ||
incident_directions : :py:class:`~pyfar.classes.coordinates.Coordinates` | ||
Defines the incidence directions of each `scattering_coefficients` | ||
in a :py:class:`~pyfar.classes.coordinates.Coordinates` object. | ||
Its cshape needs to match | ||
the last dimension of `scattering_coefficients`. | ||
Points contained in `incident_directions` must have the same radii. | ||
The weights need to reflect the area `incident_directions.weights`. | ||
|
||
Returns | ||
------- | ||
random_scattering : :py:class:`~pyfar.classes.audio.FrequencyData` | ||
The random-incidence scattering coefficient depending on frequency. | ||
|
||
References | ||
---------- | ||
.. [2] H. Kuttruff, Room acoustics, Sixth edition. Boca Raton: | ||
CRC Press/Taylor & Francis Group, 2017. | ||
""" | ||
random_scattering = utils.paris_formula( | ||
scattering_coefficients, incident_directions) | ||
return random_scattering |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from .utils import ( | ||
paris_formula, | ||
) | ||
|
||
__all__ = [ | ||
'paris_formula', | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import numpy as np | ||
import pyfar as pf | ||
|
||
|
||
def paris_formula(coefficients, incident_directions): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name of the function and module suggests that it is public. In this case should we add it to the docs as well? |
||
r""" | ||
Calculate the random-incidence coefficient from free-field | ||
data for several incident directions. | ||
|
||
Uses the Paris formula [2]_. | ||
|
||
.. math:: | ||
c_{rand} = \sum c(\vartheta,\varphi) \cdot cos(\vartheta) \cdot | ||
w(\vartheta,\varphi) | ||
|
||
with the coefficients :math:`c(\vartheta,\varphi)`, the area | ||
weights ``w`` taken from the `incident_directions.weights`, | ||
and :math:`\vartheta` and :math:`\varphi` are the ``colatitude`` | ||
angle and ``azimuth`` angles from the | ||
:py:class:`~pyfar.classes.coordinates.Coordinates` object. | ||
Note that the incident directions should be | ||
equally distributed to get a valid result. | ||
|
||
Parameters | ||
---------- | ||
coefficients : :py:class:`~pyfar.classes.audio.FrequencyData` | ||
Scattering coefficients for different incident directions. Its cshape | ||
needs to be (..., `incident_directions.csize`) | ||
incident_directions : :py:class:`~pyfar.classes.coordinates.Coordinates` | ||
Defines the incidence directions of each `scattering_coefficients` | ||
in a :py:class:`~pyfar.classes.coordinates.Coordinates` object. | ||
Its cshape needs to match | ||
the last dimension of `scattering_coefficients`. | ||
Points contained in `incident_directions` must have the same radii. | ||
The weights need to reflect the area ``incident_directions.weights``. | ||
|
||
Returns | ||
------- | ||
random_coefficient : :py:class:`~pyfar.classes.audio.FrequencyData` | ||
The random-incidence coefficient depending on frequency. | ||
|
||
References | ||
---------- | ||
.. [2] H. Kuttruff, Room acoustics, Sixth edition. Boca Raton: | ||
CRC Press/Taylor & Francis Group, 2017. | ||
""" | ||
if not isinstance(coefficients, pf.FrequencyData): | ||
raise ValueError("coefficients has to be FrequencyData") | ||
if not isinstance(incident_directions, pf.Coordinates): | ||
raise ValueError("incident_directions have to be None or Coordinates") | ||
if incident_directions.cshape[0] != coefficients.cshape[-1]: | ||
raise ValueError( | ||
"the last dimension of coefficients needs be same as " | ||
"the incident_directions.cshape.") | ||
|
||
theta = incident_directions.get_sph().T[1] | ||
weight = np.cos(theta) * incident_directions.weights | ||
norm = np.sum(weight) | ||
coefficients_freq = np.swapaxes(coefficients.freq, -1, -2) | ||
random_coefficient = pf.FrequencyData( | ||
np.sum(coefficients_freq*weight/norm, axis=-1), | ||
coefficients.frequencies, | ||
comment='random-incidence coefficient' | ||
) | ||
return random_coefficient |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import pytest | ||
import pyfar as pf | ||
import numpy as np | ||
|
||
|
||
@pytest.fixture | ||
def half_sphere(): | ||
"""return 42th order gaussian sampling for the half sphere and radius 1. | ||
|
||
Returns | ||
------- | ||
pf.Coordinates | ||
half sphere sampling grid | ||
""" | ||
mics = pf.samplings.sph_gaussian(42) | ||
# delete lower part of sphere | ||
return mics[mics.get_sph().T[1] <= np.pi/2] | ||
|
||
|
||
@pytest.fixture | ||
def quarter_half_sphere(): | ||
"""return 10th order gaussian sampling for the quarter half sphere | ||
and radius 1. | ||
|
||
Returns | ||
------- | ||
pf.Coordinates | ||
quarter half sphere sampling grid | ||
""" | ||
incident_directions = pf.samplings.sph_gaussian(10) | ||
incident_directions = incident_directions[ | ||
incident_directions.get_sph().T[1] <= np.pi/2] | ||
return incident_directions[ | ||
incident_directions.get_sph().T[0] <= np.pi/2] | ||
|
||
|
||
@pytest.fixture | ||
def pressure_data_mics(half_sphere): | ||
"""returns a sound pressure data example, with sound pressure 0 and | ||
two frequency bins | ||
|
||
Parameters | ||
---------- | ||
half_sphere : pf.Coordinates | ||
half sphere sampling grid for mics | ||
|
||
Returns | ||
------- | ||
pyfar.FrequencyData | ||
output sound pressure data | ||
""" | ||
frequencies = [200, 300] | ||
shape_new = np.append(half_sphere.cshape, len(frequencies)) | ||
return pf.FrequencyData(np.zeros(shape_new), frequencies) | ||
|
||
|
||
@pytest.fixture | ||
def pressure_data_mics_incident_directions( | ||
half_sphere, quarter_half_sphere): | ||
"""returns a sound pressure data example, with sound pressure 0 and | ||
two frequency bins | ||
|
||
Parameters | ||
---------- | ||
half_sphere : pf.Coordinates | ||
half sphere sampling grid for mics | ||
quarter_half_sphere : pf.Coordinates | ||
quarter half sphere sampling grid for incident directions | ||
|
||
Returns | ||
------- | ||
pyfar.FrequencyData | ||
output sound pressure data | ||
""" | ||
frequencies = [200, 300] | ||
shape_new = np.append( | ||
quarter_half_sphere.cshape, half_sphere.cshape) | ||
shape_new = np.append(shape_new, len(frequencies)) | ||
return pf.FrequencyData(np.zeros(shape_new), frequencies) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A brief general description of the module in a sentence or two might be nice to add.