-
Notifications
You must be signed in to change notification settings - Fork 2
Scattering-freefield #8
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
Changes from all commits
2616155
5381d3b
c0ddd18
9663447
647e2bb
3c2584d
e0e4aa1
e83d947
3bb2252
8cb5b1a
12344e9
92fd361
bb27c7a
8886b4e
575de91
2880a59
d351258
d8b9bde
d8812d4
6ab3171
1546bc2
0c4ea42
37dab64
7ee83f0
56a7970
6e020a8
a1fc6db
00bd4e8
9874c81
651880e
73825ae
d4ff85e
344ab7d
42193aa
345c795
11d019a
0e6205f
b5d4a9a
dcdb264
f58168a
fe1337e
b3fe3bd
36c28bf
698f1af
93901db
c003c45
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 |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| imkar.integrate | ||
| =============== | ||
|
|
||
| .. automodule:: imkar.integrate | ||
| :members: | ||
| :undoc-members: | ||
| :show-inheritance: |
| 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: |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| from .integrate import ( | ||
| surface_sphere | ||
| ) | ||
|
|
||
|
|
||
| __all__ = [ | ||
| 'surface_sphere'] |
| 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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| from .coefficient import ( | ||
| random_incidence, | ||
| freefield | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| 'freefield', | ||
| 'random_incidence' | ||
| ] |
| 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 |
| 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'] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| """ | ||
| 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 spherical_coordinates(phi_rad, theta_rad, r=1): | ||
| phi, theta = np.meshgrid(phi_rad, theta_rad) | ||
| coords = pf.Coordinates( | ||
| phi, theta, np.ones(phi.shape)*r, 'sph') | ||
| return coords | ||
|
|
||
|
|
||
| def frequencydata_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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,3 +11,5 @@ pytest | |
| numpy | ||
| scipy | ||
| pyfar | ||
| insipid-sphinx-theme | ||
| autodocsumm | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import pytest | ||
| import numpy as np | ||
| from imkar.testing import stub_utils | ||
|
|
||
|
|
||
| @pytest.fixture | ||
|
Member
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. maybe I'm missing something here: What's the reason for not using pyfar.samplings?
Member
Author
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 shape of the data is quite different, pyfar.samplings just returns a flatten Coordinates object. we need a cshape of (#angle_1, #angle_2) |
||
| def coords_half_sphere_1_deg(): | ||
| delta = 1 | ||
| return stub_utils.spherical_coordinates( | ||
| np.linspace(0, 2*np.pi, num=int(360/delta)+1), | ||
| np.linspace(0, np.pi/2, num=int(90/delta)+1)) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def coords_half_sphere_5_deg(): | ||
| delta = 5 | ||
| return stub_utils.spherical_coordinates( | ||
| np.linspace(0, 2*np.pi, num=int(360/delta)+1), | ||
| np.linspace(0, np.pi/2, num=int(90/delta)+1)) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def coords_half_sphere_10_deg(): | ||
| delta = 10 | ||
| return stub_utils.spherical_coordinates( | ||
| np.linspace(0, 2*np.pi, num=int(360/delta)+1), | ||
| np.linspace(0, np.pi/2, num=int(90/delta)+1)) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def coords_half_sphere_30_deg(): | ||
| delta = 30 | ||
| return stub_utils.spherical_coordinates( | ||
| np.linspace(0, 2*np.pi, num=int(360/delta)+1), | ||
| np.linspace(0, np.pi/2, num=int(90/delta)+1)) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def coords_half_sphere_45_deg(): | ||
| delta = 45 | ||
| return stub_utils.spherical_coordinates( | ||
| np.linspace(0, 2*np.pi, num=int(360/delta)+1), | ||
| np.linspace(0, np.pi/2, num=int(90/delta)+1)) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def coords_sphere_10_deg(): | ||
| delta = 10 | ||
| return stub_utils.spherical_coordinates( | ||
| np.linspace(0, 2*np.pi, num=int(360/delta)+1), | ||
| np.linspace(0, np.pi, num=int(180/delta)+1)) | ||
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.
If you really want to provide stub utils, please add test for them