-
Notifications
You must be signed in to change notification settings - Fork 2
Feature: correlation method after mommertz #31
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
base: develop
Are you sure you want to change the base?
Changes from all commits
f484231
6ac7a4d
a865d2c
ae1a7c3
026f804
fba1ada
67de445
051fb26
205f481
0e7714a
06581ba
51e459b
232bc97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| imkar.scattering.freefield | ||
| ========================== | ||
|
|
||
| .. automodule:: imkar.scattering.freefield | ||
| :members: | ||
| :undoc-members: | ||
| :show-inheritance: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| """Imkar scattering module.""" | ||
|
|
||
| from . import freefield | ||
|
|
||
| __all__ = [ | ||
| "freefield", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| """Scattering calculation functions based on free-field data.""" | ||
| import numpy as np | ||
| import pyfar as pf | ||
|
|
||
|
|
||
| def correlation_method( | ||
| sample_pressure, reference_pressure, microphone_weights): | ||
| r""" | ||
| Calculate the scattering coefficient from free-field reflection pattern. | ||
|
|
||
| This function uses the Mommertz correlation method [#]_ to compute the | ||
| scattering coefficient by averaging over the microphone positions: | ||
|
|
||
| .. math:: | ||
| s = 1 - | ||
| \frac{|\sum_{\Omega_r} \underline{p}_{\text{sample}}(\Omega_r) | ||
| \cdot \underline{p}_{\text{reference}}^*(\Omega_r) | ||
| \cdot w(\Omega_r)|^2} | ||
| {\sum_{\Omega_r} |\underline{p}_{\text{sample}}(\Omega_r)|^2 | ||
| \cdot w(\Omega_r) \cdot \sum_{\Omega_r} | ||
| |\underline{p}_{\text{reference}}(\Omega_r)|^2 | ||
| \cdot w(\Omega_r) } | ||
|
|
||
| where: | ||
| - :math:`\underline{p}_{\text{sample}}` is the reflected sound | ||
| pressure of the sample under investigation. | ||
| - :math:`\underline{p}_{\text{reference}}` is the reflected sound | ||
| pressure from the reference sample. | ||
| - :math:`\Omega_r` represents the solid angle of the microphone | ||
| positions and :math:`w(\Omega_r)` represents its area weight. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| sample_pressure : pyfar.FrequencyData, pyfar.Signal | ||
| Reflected sound pressure or directivity of the test sample. Its | ||
| `cshape` must be ``(..., microphone_weights.size)`` and broadcastable | ||
| to the `cshape` of ``reference_pressure``. The frequency vectors | ||
| of both ``sample_pressure`` and ``reference_pressure`` must match. | ||
| reference_pressure : pyfar.FrequencyData, pyfar.Signal | ||
| Reflected sound pressure or directivity of the reference sample. Its | ||
|
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. What is meant with the "or"? Do reflected sound pressure and directivity mean the same thing or can both be used as input for the function? 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. both can be used, because it is proportional |
||
| `cshape` must be (..., microphone_weights.size) and broadcastable | ||
| to the `cshape` of ``sample_pressure``. The frequency vectors of both | ||
| ``sample_pressure`` and ``reference_pressure`` must match. | ||
| microphone_weights : array_like | ||
| 1D array containing the area weights for the microphone positions. | ||
| No normalization is required. Its shape must match the last dimension | ||
| in the `cshape` of ``sample_pressure`` and ``reference_pressure``. | ||
|
|
||
| Returns | ||
| ------- | ||
| scattering_coefficients : pyfar.FrequencyData | ||
| The scattering coefficient of the broadcasted `cshape` of | ||
| ``sample_pressure`` and ``reference_pressure``, excluding the | ||
| last dimension. | ||
|
|
||
| References | ||
| ---------- | ||
| .. [#] E. Mommertz, "Determination of scattering coefficients from the | ||
| reflection directivity of architectural surfaces," Applied | ||
| Acoustics, vol. 60, no. 2, pp. 201-203, June 2000, | ||
| doi: 10.1016/S0003-682X(99)00057-2. | ||
|
|
||
| """ | ||
| # check input types | ||
| if not isinstance(sample_pressure, (pf.FrequencyData, pf.Signal)): | ||
| raise TypeError( | ||
| "sample_pressure must be of type pyfar.FrequencyData or " | ||
| "pyfar.Signal") | ||
| if not isinstance(reference_pressure, (pf.FrequencyData, pf.Signal)): | ||
| raise TypeError( | ||
| "reference_pressure must be of type pyfar.FrequencyData or " | ||
| "pyfar.Signal") | ||
ahms5 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| microphone_weights = np.atleast_1d( | ||
| np.asarray(microphone_weights, dtype=float)) | ||
|
|
||
| # check input dimensions | ||
| if sample_pressure.cshape[-1] != microphone_weights.size: | ||
| raise ValueError( | ||
| "The last dimension of sample_pressure must match the size of " | ||
| "microphone_weights") | ||
| if reference_pressure.cshape[-1] != microphone_weights.size: | ||
ahms5 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| raise ValueError( | ||
| "The last dimension of reference_pressure must match the size of " | ||
| "microphone_weights") | ||
| # check frequency vectors | ||
| if not np.allclose( | ||
| reference_pressure.frequencies, sample_pressure.frequencies, | ||
| atol=1e-15): | ||
| raise ValueError("The frequencies do not match.") | ||
|
|
||
ahms5 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # prepare data | ||
| microphone_weights = microphone_weights[:, np.newaxis] | ||
| p_sample = sample_pressure.freq | ||
| p_reference = reference_pressure.freq | ||
|
|
||
| # calculate according to mommertz correlation method Equation (5) | ||
| p_sample_sum = np.sum(microphone_weights * np.abs(p_sample)**2, axis=-2) | ||
| p_ref_sum = np.sum(microphone_weights * np.abs(p_reference)**2, axis=-2) | ||
| p_cross_sum = np.sum( | ||
| p_sample * np.conj(p_reference) * microphone_weights, axis=-2) | ||
|
|
||
| data_scattering_coefficient = 1 - np.abs(p_cross_sum)**2/( | ||
| p_sample_sum*p_ref_sum) | ||
|
Comment on lines
+97
to
+103
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 would be cool to have pyfar functions sum, abs, conj in the future, wouldn't it? pyfar/pyfar#830 |
||
|
|
||
| # create pyfar.FrequencyData object | ||
| scattering_coefficients = pf.FrequencyData( | ||
| data_scattering_coefficient, | ||
| sample_pressure.frequencies) | ||
|
|
||
| return scattering_coefficients | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| import pyfar as pf | ||
| import numpy as np | ||
| import numpy.testing as npt | ||
| import pytest | ||
| from imkar.scattering import freefield as sff | ||
|
|
||
|
|
||
| def plane_wave(amplitude, direction, sampling): | ||
|
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. Please add a minimal docstring to ease maintainance |
||
| """Generate a plane wave for a given direction and sampling. | ||
|
|
||
| This function is just used for testing purposes, so the frequency is | ||
| fixed to 5000 Hz. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| amplitude : float | ||
| The amplitude of the plane wave. | ||
| direction : pf.Coordinates | ||
| The direction of the plane wave. | ||
| sampling : pf.Sampling | ||
| The sampling grid for the plane wave. | ||
|
|
||
| Returns | ||
| ------- | ||
| pf.FrequencyData | ||
| The generated plane wave in the frequency domain. | ||
| """ | ||
|
|
||
| f = 5000 | ||
| c = 343 | ||
| x = sampling | ||
| direction.cartesian = direction.cartesian/direction.radius | ||
| dot_product = direction.x*x.x+direction.y*x.y+direction.z*x.z | ||
| dot_product = dot_product[..., np.newaxis] | ||
| f = np.atleast_1d(f) | ||
| return pf.FrequencyData( | ||
| amplitude*np.exp(-1j*2*np.pi*f/c*dot_product), | ||
| frequencies=f, | ||
| ) | ||
|
|
||
|
|
||
| def test_correlation_zero_scattering(): | ||
| sampling = pf.samplings.sph_equal_area(5000) | ||
|
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. Better already use samplings from spharpy? 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'll shift it to the todo, after the spharpy release |
||
| sampling.weights = pf.samplings.calculate_sph_voronoi_weights(sampling) | ||
| sampling = sampling[sampling.z>0] | ||
| sample_pressure = plane_wave(1, pf.Coordinates(0, 0, 1), sampling) | ||
| reference_pressure = plane_wave(1, pf.Coordinates(0, 0, 1), sampling) | ||
| s = sff.correlation_method( | ||
| sample_pressure, reference_pressure, sampling.weights, | ||
| ) | ||
| npt.assert_almost_equal(s.freq, 0) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("s_scatter", [0.1, 0.5, 0.9]) | ||
| @pytest.mark.parametrize("Phi_scatter_deg", [30, 60, 90, 120, 150, 42]) | ||
| def test_correlation_fractional_scattering(s_scatter, Phi_scatter_deg): | ||
| """ | ||
| Test analytic scattering coefficient calculation for non 0 or 1. | ||
|
|
||
| For the sample pressure, two plane waves are used. The scattered and the | ||
| specular wave. The reflection factor of both waves is calculated based on | ||
| the scattering coefficient and the scattering angle. | ||
| """ | ||
| # calculate the specular and scattered reflection factors | ||
| s_spec = 1-s_scatter | ||
| Phi_spec = 45/180*np.pi | ||
| Phi_scatter = Phi_scatter_deg/180*np.pi | ||
| R_spec = np.sqrt(s_spec) | ||
| R_scatter = np.sqrt(np.abs(s_scatter*np.sin(Phi_spec)/np.sin(Phi_scatter))) | ||
|
|
||
| # define the sampling grid and generate the plane waves | ||
| # for the sample pressure and the reference pressure | ||
| sampling = pf.samplings.sph_equal_area(10000) | ||
| sampling.weights = pf.samplings.calculate_sph_voronoi_weights(sampling) | ||
| sampling = sampling[sampling.z>0] | ||
| sample_pressure = plane_wave( | ||
| R_spec, | ||
| pf.Coordinates.from_spherical_front(np.pi/2, Phi_spec, 1), sampling) | ||
| sample_pressure += plane_wave( | ||
| R_scatter, | ||
| pf.Coordinates.from_spherical_front(np.pi/2, Phi_scatter, 1), sampling) | ||
|
|
||
| # Calculate the scattering coefficient with for the specular wave | ||
| # in other words, the reference pressure is the specular wave (R=1) | ||
| reference_pressure = plane_wave( | ||
| 1, pf.Coordinates.from_spherical_front(np.pi/2, Phi_spec, 1), sampling) | ||
| sd_spec = 1-sff.correlation_method( | ||
| sample_pressure, reference_pressure, sampling.weights, | ||
| ) | ||
| npt.assert_almost_equal(sd_spec.freq, s_spec, 1) | ||
|
|
||
| # Calculate the scattering coefficient for the scattered wave | ||
| # in other words, the reference pressure is the scattered wave (R=1) | ||
| # so the result will be 1-s_scatter | ||
| reference_pressure = plane_wave( | ||
| 1, pf.Coordinates.from_spherical_front( | ||
| np.pi/2, Phi_scatter, 1), sampling) | ||
| sd_scatter = 1-sff.correlation_method( | ||
| sample_pressure, reference_pressure, sampling.weights, | ||
| ) | ||
| npt.assert_almost_equal(sd_scatter.freq, s_scatter, 1) | ||
|
|
||
| # Calculate the scattering coefficient for 5° more than the specular | ||
| # wave, then the scattering coefficient should be 0 | ||
| reference_pressure = plane_wave( | ||
| 1, pf.Coordinates.from_spherical_front( | ||
| np.pi/2, Phi_spec+5/180*np.pi, 1), sampling) | ||
| sd_scatter_0 = 1-sff.correlation_method( | ||
| sample_pressure, reference_pressure, sampling.weights, | ||
| ) | ||
| npt.assert_almost_equal(sd_scatter_0.freq, 0, 1) | ||
|
|
||
|
|
||
| def test_correlation_one_scattering(): | ||
|
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. Short docstrings would be helpful :) |
||
| """Two plane waves in the different directions should result in s=1.""" | ||
| sampling = pf.samplings.sph_equal_area(5000) | ||
| sampling.weights = pf.samplings.calculate_sph_voronoi_weights(sampling) | ||
| sampling = sampling[sampling.z>0] | ||
| sample_pressure = plane_wave(1, pf.Coordinates(0, 1, 0), sampling) | ||
| reference_pressure = plane_wave(1, pf.Coordinates(0, 0, 1), sampling) | ||
| s = sff.correlation_method( | ||
| sample_pressure, reference_pressure, sampling.weights, | ||
| ) | ||
| npt.assert_almost_equal(s.freq, 1, decimal=3) | ||
|
|
||
|
|
||
| def test_correlation_method_invalid_sample_pressure_type(): | ||
| reference_pressure = pf.FrequencyData(np.array([1, 2, 3]), [100, 200, 300]) | ||
| microphone_weights = np.array([0.5, 0.5, 0.5]) | ||
| with pytest.raises( | ||
| TypeError, match="sample_pressure must be of type " | ||
| "pyfar.FrequencyData"): | ||
| sff.correlation_method( | ||
| "invalid_type", reference_pressure, microphone_weights) | ||
|
|
||
|
|
||
| def test_correlation_method_invalid_reference_pressure_type(): | ||
| sample_pressure = pf.FrequencyData(np.array([1, 2, 3]), [100, 200, 300]) | ||
| microphone_weights = np.array([0.5, 0.5, 0.5]) | ||
| with pytest.raises( | ||
| TypeError, match="reference_pressure must be of type " | ||
| "pyfar.FrequencyData"): | ||
| sff.correlation_method( | ||
| sample_pressure, "invalid_type", microphone_weights) | ||
|
|
||
|
|
||
| def test_correlation_method_mismatched_sample_pressure_weights(): | ||
| sample_pressure = pf.FrequencyData(np.array([[1, 2, 3]]).T, [100]) | ||
| reference_pressure = pf.FrequencyData( | ||
| np.array([[1, 2]]).T, [100]) | ||
| microphone_weights = np.array([0.5, 0.5]) | ||
| with pytest.raises( | ||
| ValueError, match="The last dimension of sample_pressure must " | ||
| "match the size of microphone_weights"): | ||
| sff.correlation_method( | ||
| sample_pressure, reference_pressure, microphone_weights) | ||
|
|
||
|
|
||
| def test_correlation_method_mismatched_reference_pressure_weights(): | ||
| sample_pressure = pf.FrequencyData(np.array([[1, 2]]).T, [100]) | ||
| reference_pressure = pf.FrequencyData( | ||
| np.array([[1, 2, 3]]).T, [100]) | ||
| microphone_weights = np.array([0.5, 0.5]) | ||
| with pytest.raises( | ||
| ValueError, match="The last dimension of reference_pressure must " | ||
| "match the size of microphone_weights"): | ||
| sff.correlation_method( | ||
| sample_pressure, reference_pressure, microphone_weights) | ||
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.
The name 'Mommertz' appears to be misspelled. Based on the reference cited (line 59), it should be 'Mommertz' consistently throughout, but please verify the correct spelling of the author's name.