-
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 15 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 |
---|---|---|
|
@@ -7,4 +7,4 @@ according to their modules. | |
.. toctree:: | ||
:maxdepth: 1 | ||
|
||
|
||
modules/imkar.scattering |
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,9 @@ | ||
from .scattering import ( | ||
freefield, | ||
random, | ||
) | ||
|
||
__all__ = [ | ||
'freefield', | ||
'random', | ||
] |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,129 @@ | ||||||||||||||
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 free-field scattering coefficient for each incident direction | ||||||||||||||
using the Mommertz correlation method [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. Should we go full Zen of Python and start with a one liner without reference? I would also not use incident direction, since this is not passed and seems to be implicitly contained in the input data.
Suggested change
|
||||||||||||||
|
||||||||||||||
.. math:: | ||||||||||||||
s(\vartheta_S,\varphi_S) = 1 - | ||||||||||||||
\frac{|\sum \underline{p}_{sample}(\vartheta_R,\varphi_R) \cdot | ||||||||||||||
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. For readability I would suggest to use text in the subscripts, e.g.: |
||||||||||||||
\underline{p}_{reference}^*(\vartheta_R,\varphi_R) \cdot w|^2} | ||||||||||||||
{\sum |\underline{p}_{sample}(\vartheta_R,\varphi_R)|^2 \cdot w | ||||||||||||||
\cdot \sum |\underline{p}_{reference}(\vartheta_R,\varphi_R)|^2 | ||||||||||||||
\cdot w } | ||||||||||||||
|
||||||||||||||
with the ``sample_pressure``, the ``reference_pressure``, and the | ||||||||||||||
area weights ``weights_microphones``. See | ||||||||||||||
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.
|
||||||||||||||
:py:func:`random_incidence` to calculate the random incidence | ||||||||||||||
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. Broken reference, should be
Suggested change
tilde only displays the function name. |
||||||||||||||
scattering coefficient. | ||||||||||||||
|
||||||||||||||
Parameters | ||||||||||||||
---------- | ||||||||||||||
sample_pressure : pyfar.FrequencyData | ||||||||||||||
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 think of a way to make this usable with 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 think we should not make it extra complex, its the users responsibilty to convert it to FrequencyData. we could think to allow signals by just using the freq component of it, so without any filtering. then its stay more more open |
||||||||||||||
Reflected sound pressure or directivity of the test sample. Its cshape | ||||||||||||||
need to be (..., #microphones). | ||||||||||||||
reference_pressure : pyfar.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 : np.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. Could the be an array like and we use |
||||||||||||||
Array containing the area weights for the microphone positions. | ||||||||||||||
Its shape needs to be (#microphones), so it matches the last dimension | ||||||||||||||
in the cshape of `sample_pressure` and `reference_pressure`. | ||||||||||||||
|
||||||||||||||
Returns | ||||||||||||||
------- | ||||||||||||||
scattering_coefficients : pyfar.FrequencyData | ||||||||||||||
The scattering coefficient for each incident direction. | ||||||||||||||
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. Mention the frequency dependency of the output. |
||||||||||||||
|
||||||||||||||
|
||||||||||||||
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") | ||||||||||||||
if not isinstance(microphone_weights, np.ndarray): | ||||||||||||||
raise ValueError("microphone_weights have to be a numpy.array") | ||||||||||||||
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 need be same as the " | ||||||||||||||
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.
Suggested change
|
||||||||||||||
"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) | ||||||||||||||
scattering_coefficients.comment = 'scattering coefficient' | ||||||||||||||
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 omitt adding the comment here, to stick more clearly to our 'comments are things the user might or might not make use of'-policy? |
||||||||||||||
|
||||||||||||||
return scattering_coefficients | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
def random( | ||||||||||||||
scattering_coefficients, incident_directions): | ||||||||||||||
r"""Calculate the random-incidence scattering coefficient | ||||||||||||||
according to Paris formula. | ||||||||||||||
|
||||||||||||||
.. math:: | ||||||||||||||
s_{rand} = \sum s(\vartheta_S,\varphi_S) \cdot cos(\vartheta_S) \cdot w | ||||||||||||||
|
||||||||||||||
with the ``scattering_coefficients``, and the | ||||||||||||||
area weights ``w`` from the ``incident_directions``. | ||||||||||||||
Note that the incident directions should be | ||||||||||||||
equally distributed to get a valid result. | ||||||||||||||
|
||||||||||||||
Parameters | ||||||||||||||
---------- | ||||||||||||||
scattering_coefficients : pyfar.FrequencyData | ||||||||||||||
Scattering coefficients for different incident directions. Its cshape | ||||||||||||||
need to be (..., #source_directions) | ||||||||||||||
incident_directions : pyfar.Coordinates | ||||||||||||||
Defines the incidence directions of each `scattering_coefficients` in a | ||||||||||||||
Coordinates object. Its cshape need to be (#source_directions). In | ||||||||||||||
sperical coordinates the radii need to be constant. The weights need | ||||||||||||||
to reflect the area weights. | ||||||||||||||
|
||||||||||||||
Returns | ||||||||||||||
------- | ||||||||||||||
random_scattering : pyfar.FrequencyData | ||||||||||||||
The random-incidence scattering coefficient. | ||||||||||||||
""" | ||||||||||||||
random_scattering = utils.paris_formula( | ||||||||||||||
scattering_coefficients, incident_directions) | ||||||||||||||
random_scattering.comment = 'random-incidence scattering coefficient' | ||||||||||||||
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 above for question about using/not using comments |
||||||||||||||
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,51 @@ | ||||||
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 | ||||||
according to Paris formula. | ||||||
|
||||||
.. math:: | ||||||
c_{rand} = \sum c(\vartheta_S,\varphi_S) \cdot cos(\vartheta_S) \cdot w | ||||||
|
||||||
with the ``coefficients``, and the | ||||||
area weights ``w`` from the ``incident_directions``. | ||||||
Note that the incident directions should be | ||||||
equally distributed to get a valid result. | ||||||
|
||||||
Parameters | ||||||
---------- | ||||||
coefficients : pyfar.FrequencyData | ||||||
coefficients for different incident directions. Its cshape | ||||||
need to be (..., #incident_directions) | ||||||
ahms5 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
incident_directions : pyfar.Coordinates | ||||||
Defines the incidence directions of each `coefficients` in a | ||||||
Coordinates object. Its cshape need to be (#incident_directions). In | ||||||
ahms5 marked this conversation as resolved.
Show resolved
Hide resolved
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.
Suggested change
|
||||||
sperical coordinates the radii need to be constant. The weights need | ||||||
to reflect the area weights. | ||||||
|
||||||
Returns | ||||||
------- | ||||||
random_coefficient : pyfar.FrequencyData | ||||||
The random-incidence scattering coefficient. | ||||||
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 don't know how common the Paris formula is, but it might be nice to add a citation here as well. |
||||||
""" | ||||||
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 need 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.