From a283387e3c71889fb0bbb5d38d5ea37df84766dc Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 24 Feb 2023 17:10:35 +0100 Subject: [PATCH 01/32] impl new approach for scattering --- docs/modules.rst | 2 +- docs/modules/imkar.scattering.coefficient.rst | 7 + imkar/__init__.py | 3 + imkar/scattering/__init__.py | 9 + imkar/scattering/coefficient.py | 127 +++++++++++ imkar/testing/__init__.py | 7 + imkar/testing/stub_utils.py | 22 ++ tests/test_imkar.py | 4 +- tests/test_scattering_coefficient.py | 198 ++++++++++++++++++ tests/test_sub_utils.py | 37 ++++ 10 files changed, 413 insertions(+), 3 deletions(-) create mode 100644 docs/modules/imkar.scattering.coefficient.rst create mode 100644 imkar/scattering/__init__.py create mode 100644 imkar/scattering/coefficient.py create mode 100644 imkar/testing/__init__.py create mode 100644 imkar/testing/stub_utils.py create mode 100644 tests/test_scattering_coefficient.py create mode 100644 tests/test_sub_utils.py diff --git a/docs/modules.rst b/docs/modules.rst index 52a15ec..9da889f 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -7,4 +7,4 @@ according to their modules. .. toctree:: :maxdepth: 1 - + modules/imkar.scattering.coefficient diff --git a/docs/modules/imkar.scattering.coefficient.rst b/docs/modules/imkar.scattering.coefficient.rst new file mode 100644 index 0000000..f384e68 --- /dev/null +++ b/docs/modules/imkar.scattering.coefficient.rst @@ -0,0 +1,7 @@ +imkar.scattering.coefficient +============================ + +.. automodule:: imkar.scattering.coefficient + :members: + :undoc-members: + :show-inheritance: diff --git a/imkar/__init__.py b/imkar/__init__.py index 2d1e55d..e176307 100644 --- a/imkar/__init__.py +++ b/imkar/__init__.py @@ -3,3 +3,6 @@ __author__ = """The pyfar developers""" __email__ = 'info@pyfar.org' __version__ = '0.1.0' + + +from . import scattering # noqa diff --git a/imkar/scattering/__init__.py b/imkar/scattering/__init__.py new file mode 100644 index 0000000..5e3fc4d --- /dev/null +++ b/imkar/scattering/__init__.py @@ -0,0 +1,9 @@ +from .coefficient import ( + random_incidence, + freefield +) + +__all__ = [ + 'freefield', + 'random_incidence' + ] diff --git a/imkar/scattering/coefficient.py b/imkar/scattering/coefficient.py new file mode 100644 index 0000000..6dbc11f --- /dev/null +++ b/imkar/scattering/coefficient.py @@ -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 diff --git a/imkar/testing/__init__.py b/imkar/testing/__init__.py new file mode 100644 index 0000000..dc61984 --- /dev/null +++ b/imkar/testing/__init__.py @@ -0,0 +1,7 @@ +from .stub_utils import ( + frequency_data_from_shape, +) + +__all__ = [ + 'frequency_data_from_shape', + ] diff --git a/imkar/testing/stub_utils.py b/imkar/testing/stub_utils.py new file mode 100644 index 0000000..3375398 --- /dev/null +++ b/imkar/testing/stub_utils.py @@ -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 diff --git a/tests/test_imkar.py b/tests/test_imkar.py index f801097..6b6c622 100644 --- a/tests/test_imkar.py +++ b/tests/test_imkar.py @@ -5,7 +5,7 @@ import pytest -from imkar import imkar +from imkar import imkar # noqa @pytest.fixture @@ -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 diff --git a/tests/test_scattering_coefficient.py b/tests/test_scattering_coefficient.py new file mode 100644 index 0000000..22c9717 --- /dev/null +++ b/tests/test_scattering_coefficient.py @@ -0,0 +1,198 @@ +import pytest +import numpy as np +import pyfar as pf + +from imkar import scattering +from imkar.testing import stub_utils + + +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +def test_freefield_1(frequencies): + mics = pf.samplings.sph_gaussian(42) + mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere + p_sample = stub_utils.frequency_data_from_shape( + mics.cshape, 1, frequencies) + p_reference = stub_utils.frequency_data_from_shape( + mics.cshape, 0, frequencies) + p_sample.freq[5, :] = 0 + p_reference.freq[5, :] = np.sum(p_sample.freq.flatten())/2 + s = scattering.coefficient.freefield(p_sample, p_reference, mics.weights) + np.testing.assert_allclose(s.freq, 1) + + +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +def test_freefield_wrong_input(frequencies): + mics = pf.samplings.sph_gaussian(42) + mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere + p_sample = stub_utils.frequency_data_from_shape( + mics.cshape, 1, frequencies) + p_reference = stub_utils.frequency_data_from_shape( + mics.cshape, 0, frequencies) + p_sample.freq[5, :] = 0 + p_reference.freq[5, :] = np.sum(p_sample.freq.flatten())/2 + with pytest.raises(ValueError, match='sample_pressure'): + scattering.coefficient.freefield(1, p_reference, mics.weights) + with pytest.raises(ValueError, match='reference_pressure'): + scattering.coefficient.freefield(p_sample, 1, mics.weights) + with pytest.raises(ValueError, match='weights_microphones'): + scattering.coefficient.freefield(p_sample, p_reference, 1) + with pytest.raises(ValueError, match='cshape'): + scattering.coefficient.freefield( + p_sample[:-2, ...], p_reference, mics.weights) + with pytest.raises(ValueError, match='weights_microphones'): + scattering.coefficient.freefield( + p_sample, p_reference, mics.weights[:10]) + with pytest.raises(ValueError, match='same frequencies'): + p_sample.frequencies[0] = 1 + scattering.coefficient.freefield(p_sample, p_reference, mics.weights) + + +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +def test_freefield_05(frequencies): + mics = pf.samplings.sph_gaussian(42) + mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere + p_sample = stub_utils.frequency_data_from_shape( + mics.cshape, 0, frequencies) + p_reference = stub_utils.frequency_data_from_shape( + mics.cshape, 0, frequencies) + p_sample.freq[7, :] = 1 + p_sample.freq[28, :] = 1 + p_reference.freq[7, :] = 1 + s = scattering.coefficient.freefield(p_sample, p_reference, mics.weights) + np.testing.assert_allclose(s.freq, 0.5) + + +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +def test_freefield_0(frequencies): + mics = pf.samplings.sph_gaussian(42) + mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere + p_sample = stub_utils.frequency_data_from_shape( + mics.cshape, 0, frequencies) + p_reference = stub_utils.frequency_data_from_shape( + mics.cshape, 0, frequencies) + p_reference.freq[5, :] = 1 + p_sample.freq[5, :] = 1 + s = scattering.coefficient.freefield(p_sample, p_reference, mics.weights) + np.testing.assert_allclose(s.freq, 0) + assert s.freq.shape[-1] == len(frequencies) + + +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +def test_freefield_0_with_incident(frequencies): + mics = pf.samplings.sph_equal_area(42) + mics.weights = pf.samplings.calculate_sph_voronoi_weights(mics) + mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere + incident_directions = pf.samplings.sph_gaussian(10) + incident_directions = incident_directions[ + incident_directions.get_sph().T[1] <= np.pi/2] + incident_directions = incident_directions[ + incident_directions.get_sph().T[0] <= np.pi/2] + data_shape = np.array((incident_directions.cshape, mics.cshape)).flatten() + p_sample = stub_utils.frequency_data_from_shape( + data_shape, 0, frequencies) + p_reference = stub_utils.frequency_data_from_shape( + data_shape, 0, frequencies) + p_reference.freq[:, 2, :] = 1 + p_sample.freq[:, 2, :] = 1 + s = scattering.coefficient.freefield(p_sample, p_reference, mics.weights) + s_rand = scattering.coefficient.random_incidence(s, incident_directions) + np.testing.assert_allclose(s.freq, 0) + np.testing.assert_allclose(s_rand.freq, 0) + assert s.freq.shape[-1] == len(frequencies) + assert s.cshape == incident_directions.cshape + assert s_rand.freq.shape[-1] == len(frequencies) + + +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +def test_freefield_1_with_incidence( + frequencies): + mics = pf.samplings.sph_equal_angle(10) + mics.weights = pf.samplings.calculate_sph_voronoi_weights(mics) + mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere + incident_directions = pf.samplings.sph_gaussian(10) + incident_directions = incident_directions[ + incident_directions.get_sph().T[1] <= np.pi/2] + incident_directions = incident_directions[ + incident_directions.get_sph().T[0] <= np.pi/2] + data_shape = np.array((incident_directions.cshape, mics.cshape)).flatten() + p_sample = stub_utils.frequency_data_from_shape( + data_shape, 0, frequencies) + p_reference = stub_utils.frequency_data_from_shape( + data_shape, 0, frequencies) + p_reference.freq[:, 2, :] = 1 + p_sample.freq[:, 3, :] = 1 + s = scattering.coefficient.freefield(p_sample, p_reference, mics.weights) + s_rand = scattering.coefficient.random_incidence(s, incident_directions) + np.testing.assert_allclose(s.freq, 1) + np.testing.assert_allclose(s_rand.freq, 1) + assert s.freq.shape[-1] == len(frequencies) + assert s.cshape == incident_directions.cshape + assert s_rand.freq.shape[-1] == len(frequencies) + + +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +@pytest.mark.parametrize("mics", [ + (pf.samplings.sph_equal_angle(10)), + ]) +def test_freefield_05_with_inci(frequencies, mics): + mics.weights = pf.samplings.calculate_sph_voronoi_weights(mics) + mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere + incident_directions = pf.samplings.sph_gaussian(10) + incident_directions = incident_directions[ + incident_directions.get_sph().T[1] <= np.pi/2] + incident_directions = incident_directions[ + incident_directions.get_sph().T[0] <= np.pi/2] + data_shape = np.array((incident_directions.cshape, mics.cshape)).flatten() + p_sample = stub_utils.frequency_data_from_shape( + data_shape, 0, frequencies) + p_reference = stub_utils.frequency_data_from_shape( + data_shape, 0, frequencies) + p_sample.freq[:, 7, :] = 1 + p_sample.freq[:, 5, :] = 1 + p_reference.freq[:, 5, :] = 1 + s = scattering.coefficient.freefield(p_sample, p_reference, mics.weights) + s_rand = scattering.coefficient.random_incidence(s, incident_directions) + np.testing.assert_allclose(s.freq, 0.5) + np.testing.assert_allclose(s_rand.freq, 0.5) + assert s.freq.shape[-1] == len(frequencies) + assert s.cshape == incident_directions.cshape + assert s_rand.freq.shape[-1] == len(frequencies) + + +@pytest.mark.parametrize("s_value", [ + (0), (0.2), (0.5), (0.8), (1)]) +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +def test_random_incidence_constant_s( + s_value, frequencies): + coords = pf.samplings.sph_gaussian(10) + incident_directions = coords[coords.get_sph().T[1] <= np.pi/2] + s = stub_utils.frequency_data_from_shape( + incident_directions.cshape, s_value, frequencies) + s_rand = scattering.coefficient.random_incidence(s, incident_directions) + np.testing.assert_allclose(s_rand.freq, s_value) + + +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +def test_random_incidence_non_constant_s(frequencies): + data = pf.samplings.sph_gaussian(10) + incident_directions = data[data.get_sph().T[1] <= np.pi/2] + incident_cshape = incident_directions.cshape + s_value = np.arange( + incident_cshape[0]).reshape(incident_cshape) / incident_cshape[0] + theta = incident_directions.get_sph().T[1] + actual_weight = np.cos(theta) * incident_directions.weights + actual_weight /= np.sum(actual_weight) + s = stub_utils.frequency_data_from_shape( + [], s_value, frequencies) + s_rand = scattering.coefficient.random_incidence(s, incident_directions) + desired = np.sum(s_value*actual_weight) + np.testing.assert_allclose(s_rand.freq, desired) diff --git a/tests/test_sub_utils.py b/tests/test_sub_utils.py new file mode 100644 index 0000000..99a9003 --- /dev/null +++ b/tests/test_sub_utils.py @@ -0,0 +1,37 @@ +import pytest +import numpy as np + +from imkar.testing import stub_utils + + +@pytest.mark.parametrize( + "shapes", [ + (3, 2), + (5, 2), + (3, 2, 7), + ]) +@pytest.mark.parametrize( + "data_in", [ + 0.1, + 0, + np.array([0.1, 1]), + np.arange(4*5).reshape(4, 5), + ]) +@pytest.mark.parametrize( + "frequency", [ + [100], + [100, 200], + ]) +def test_frequency_data_from_shape(shapes, data_in, frequency): + data = stub_utils.frequency_data_from_shape(shapes, data_in, frequency) + # npt.assert_allclose(data.freq, data_in) + if hasattr(data_in, '__len__'): + for idx in range(len(data_in.shape)): + assert data.cshape[idx] == data_in.shape[idx] + for idx in range(len(shapes)): + assert data.cshape[idx+len(data_in.shape)] == shapes[idx] + + else: + for idx in range(len(shapes)): + assert data.cshape[idx] == shapes[idx] + assert data.n_bins == len(frequency) From bcb4fbc913881d918909326a11d042811524e064 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 24 Feb 2023 18:49:42 +0100 Subject: [PATCH 02/32] make diffusion module available --- imkar/__init__.py | 1 + tests/test_imkar.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/imkar/__init__.py b/imkar/__init__.py index e176307..e9d7db7 100644 --- a/imkar/__init__.py +++ b/imkar/__init__.py @@ -6,3 +6,4 @@ from . import scattering # noqa +from . import diffusion # noqa diff --git a/tests/test_imkar.py b/tests/test_imkar.py index 6b6c622..baaa6a2 100644 --- a/tests/test_imkar.py +++ b/tests/test_imkar.py @@ -22,3 +22,12 @@ 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 + + +def test_imports(): + import imkar + assert imkar + import imkar.diffusion as diffusion + assert diffusion + import imkar.scattering as scattering + assert scattering From 38eefd76dc1844f34754b3884c8ea07b2139b35e Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 24 Feb 2023 18:50:18 +0100 Subject: [PATCH 03/32] Revert "make diffusion module available" This reverts commit bcb4fbc913881d918909326a11d042811524e064. --- imkar/__init__.py | 1 - tests/test_imkar.py | 9 --------- 2 files changed, 10 deletions(-) diff --git a/imkar/__init__.py b/imkar/__init__.py index e9d7db7..e176307 100644 --- a/imkar/__init__.py +++ b/imkar/__init__.py @@ -6,4 +6,3 @@ from . import scattering # noqa -from . import diffusion # noqa diff --git a/tests/test_imkar.py b/tests/test_imkar.py index baaa6a2..6b6c622 100644 --- a/tests/test_imkar.py +++ b/tests/test_imkar.py @@ -22,12 +22,3 @@ 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 - - -def test_imports(): - import imkar - assert imkar - import imkar.diffusion as diffusion - assert diffusion - import imkar.scattering as scattering - assert scattering From a88b34fb5593193cd6ddece242c0637abd99d4f0 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 10 Mar 2023 14:58:02 +0100 Subject: [PATCH 04/32] add formula to doc --- imkar/scattering/coefficient.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/imkar/scattering/coefficient.py b/imkar/scattering/coefficient.py index 6dbc11f..86f666e 100644 --- a/imkar/scattering/coefficient.py +++ b/imkar/scattering/coefficient.py @@ -3,11 +3,21 @@ def freefield(sample_pressure, reference_pressure, weights_microphones): - """ + r""" 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. - + using the Mommertz correlation method [1]_: + + .. math:: + s(\vartheta_S,\varphi_S) = 1 - + \frac{|\sum \underline{p}_1(\vartheta_R,\varphi_R) \cdot + \underline{p}_0^*(\vartheta_R,\varphi_R) \cdot w|^2} + {\sum |\underline{p}_1(\vartheta_R,\varphi_R)|^2 \cdot w \cdot + \sum |\underline{p}_0(\vartheta_R,\varphi_R)|^2 \cdot w } + + with the ``sample_pressure`` $\underline{p}_1$, the + ``reference_pressure`` $\underline{p}_0$, and the area weights + ``weights_microphones`` $w$. See :py:func:`random_incidence` to + calculate the random incidence scattering coefficient. Parameters ---------- From 5b8a350ab57ed6714246ee23a0dbdc4a91af2392 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 10 Mar 2023 15:04:28 +0100 Subject: [PATCH 05/32] fix doc --- imkar/scattering/coefficient.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/imkar/scattering/coefficient.py b/imkar/scattering/coefficient.py index 86f666e..586fb8f 100644 --- a/imkar/scattering/coefficient.py +++ b/imkar/scattering/coefficient.py @@ -9,15 +9,15 @@ def freefield(sample_pressure, reference_pressure, weights_microphones): .. math:: s(\vartheta_S,\varphi_S) = 1 - - \frac{|\sum \underline{p}_1(\vartheta_R,\varphi_R) \cdot - \underline{p}_0^*(\vartheta_R,\varphi_R) \cdot w|^2} - {\sum |\underline{p}_1(\vartheta_R,\varphi_R)|^2 \cdot w \cdot - \sum |\underline{p}_0(\vartheta_R,\varphi_R)|^2 \cdot w } - - with the ``sample_pressure`` $\underline{p}_1$, the - ``reference_pressure`` $\underline{p}_0$, and the area weights - ``weights_microphones`` $w$. See :py:func:`random_incidence` to - calculate the random incidence scattering coefficient. + \frac{|\sum \underline{p}_{sample}(\vartheta_R,\varphi_R) \cdot + \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`` ``w``. See + :py:func:`random_incidence` to calculate the random incidence + scattering coefficient. Parameters ---------- From ac207bc9cab0b9f93b2b9a7ff15453a47cf8f00f Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 10 Mar 2023 15:32:55 +0100 Subject: [PATCH 06/32] add paris formula to docs --- imkar/scattering/coefficient.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/imkar/scattering/coefficient.py b/imkar/scattering/coefficient.py index 586fb8f..524d0df 100644 --- a/imkar/scattering/coefficient.py +++ b/imkar/scattering/coefficient.py @@ -11,8 +11,9 @@ def freefield(sample_pressure, reference_pressure, weights_microphones): s(\vartheta_S,\varphi_S) = 1 - \frac{|\sum \underline{p}_{sample}(\vartheta_R,\varphi_R) \cdot \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 } + {\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`` ``w``. See @@ -102,18 +103,24 @@ def freefield(sample_pressure, reference_pressure, weights_microphones): def random_incidence( - scattering_coefficient, incident_positions): - """Calculate the random-incidence scattering coefficient + scattering_coefficients, incident_positions): + r"""Calculate the random-incidence scattering coefficient according to Paris formula. Note that the incident directions should be equally distributed to get a valid result. + .. 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_positions``. + Parameters ---------- - scattering_coefficient : FrequencyData + scattering_coefficients : 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 + Defines the incidence directions of each `scattering_coefficients` in a Coordinates object. Its cshape need to be (#angle1, #angle2). In sperical coordinates the radii need to be constant. @@ -122,8 +129,8 @@ def random_incidence( random_scattering : FrequencyData The random-incidence scattering coefficient. """ - if not isinstance(scattering_coefficient, pf.FrequencyData): - raise ValueError("scattering_coefficient has to be FrequencyData") + if not isinstance(scattering_coefficients, pf.FrequencyData): + raise ValueError("scattering_coefficients 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") @@ -131,7 +138,7 @@ def random_incidence( 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 = scattering_coefficients*weight/norm random_scattering.freq = np.sum(random_scattering.freq, axis=-2) random_scattering.comment = 'random-incidence scattering coefficient' return random_scattering From 800b72cffa5e43cfbbb11939c40fbfa482710902 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 10 Mar 2023 15:55:15 +0100 Subject: [PATCH 07/32] fix docs --- imkar/scattering/coefficient.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/imkar/scattering/coefficient.py b/imkar/scattering/coefficient.py index 524d0df..89bcb07 100644 --- a/imkar/scattering/coefficient.py +++ b/imkar/scattering/coefficient.py @@ -22,10 +22,10 @@ def freefield(sample_pressure, reference_pressure, weights_microphones): Parameters ---------- - sample_pressure : pf.FrequencyData + sample_pressure : pyfar.FrequencyData Reflected sound pressure or directivity of the test sample. Its cshape need to be (..., #microphones). - reference_pressure : pf.FrequencyData + reference_pressure : pyfar.FrequencyData Reflected sound pressure or directivity of the test reference sample. It has the same shape as `sample_pressure`. weights_microphones : np.ndarray @@ -35,7 +35,7 @@ def freefield(sample_pressure, reference_pressure, weights_microphones): Returns ------- - scattering_coefficients : FrequencyData + scattering_coefficients : pyfar.FrequencyData The scattering coefficient for each incident direction. @@ -116,17 +116,17 @@ def random_incidence( Parameters ---------- - scattering_coefficients : FrequencyData + scattering_coefficients : pyfar.FrequencyData The scattering coefficient for each plane wave direction. Its cshape need to be (..., #angle1, #angle2) - incident_positions : pf.Coordinates + incident_positions : pyfar.Coordinates Defines the incidence directions of each `scattering_coefficients` in a Coordinates object. Its cshape need to be (#angle1, #angle2). In sperical coordinates the radii need to be constant. Returns ------- - random_scattering : FrequencyData + random_scattering : pyfar.FrequencyData The random-incidence scattering coefficient. """ if not isinstance(scattering_coefficients, pf.FrequencyData): From 2227ef4fdc331f63dea0013518458dcc6c3a0a65 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 24 Mar 2023 12:27:26 +0100 Subject: [PATCH 08/32] Apply suggestions from code review Co-authored-by: sikersten <70263411+sikersten@users.noreply.github.com> --- imkar/scattering/coefficient.py | 53 +++++++++++++-------------------- 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/imkar/scattering/coefficient.py b/imkar/scattering/coefficient.py index 89bcb07..14e059f 100644 --- a/imkar/scattering/coefficient.py +++ b/imkar/scattering/coefficient.py @@ -2,7 +2,7 @@ import pyfar as pf -def freefield(sample_pressure, reference_pressure, weights_microphones): +def freefield(sample_pressure, reference_pressure, microphone_weights): r""" Calculate the free-field scattering coefficient for each incident direction using the Mommertz correlation method [1]_: @@ -16,7 +16,7 @@ def freefield(sample_pressure, reference_pressure, weights_microphones): \cdot w } with the ``sample_pressure``, the ``reference_pressure``, and the - area weights ``weights_microphones`` ``w``. See + area weights ``weights_microphones``. See :py:func:`random_incidence` to calculate the random incidence scattering coefficient. @@ -26,12 +26,12 @@ def freefield(sample_pressure, reference_pressure, weights_microphones): 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 test - reference sample. It has the same shape as `sample_pressure`. + Reflected sound pressure or directivity of the + reference sample. Needs to have the same cshape and frequencies 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`. + 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 ------- @@ -46,29 +46,19 @@ def freefield(sample_pressure, reference_pressure, weights_microphones): 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") + 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 FrequencyData") + raise ValueError("reference_pressure has to be a pyfar.FrequencyData object") 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: + if 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]: + if weights_microphones.shape[0] != sample_pressure.cshape[-1]: raise ValueError( "the last dimension of sample_pressure need be same as the " "weights_microphones.shape.") @@ -81,10 +71,8 @@ def freefield(sample_pressure, reference_pressure, weights_microphones): # 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_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(p_sample_sq * weights_microphones, axis=-1) @@ -105,21 +93,22 @@ def freefield(sample_pressure, reference_pressure, weights_microphones): def random_incidence( scattering_coefficients, incident_positions): r"""Calculate the random-incidence scattering coefficient - according to Paris formula. Note that the incident directions should be - equally distributed to get a valid result. + 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_positions``. + Note that the incident directions should be + equally distributed to get a valid result. Parameters ---------- scattering_coefficients : pyfar.FrequencyData - The scattering coefficient for each plane wave direction. Its cshape + Scattering coefficients for different incident directions. Its cshape need to be (..., #angle1, #angle2) - incident_positions : pyfar.Coordinates + incident_directions : pyfar.Coordinates Defines the incidence directions of each `scattering_coefficients` in a Coordinates object. Its cshape need to be (#angle1, #angle2). In sperical coordinates the radii need to be constant. @@ -131,14 +120,12 @@ def random_incidence( """ if not isinstance(scattering_coefficients, pf.FrequencyData): raise ValueError("scattering_coefficients has to be FrequencyData") - if (incident_positions is not None) & \ - ~isinstance(incident_positions, pf.Coordinates): + if not 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_coefficients*weight/norm - random_scattering.freq = np.sum(random_scattering.freq, axis=-2) + random_scattering.freq = np.sum(scattering_coefficients.freq*weight/norm, axis=-2) random_scattering.comment = 'random-incidence scattering coefficient' return random_scattering From 47802e4cba4da3b7ea348998bf68d032485ff4a6 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 14 Apr 2023 13:38:38 +0200 Subject: [PATCH 09/32] new structure --- imkar/scattering/__init__.py | 6 +-- .../{coefficient.py => scattering.py} | 52 ++++++++++++------- tests/conftest.py | 17 ++++++ tests/test_scattering_coefficient.py | 42 +++++++-------- 4 files changed, 73 insertions(+), 44 deletions(-) rename imkar/scattering/{coefficient.py => scattering.py} (73%) create mode 100644 tests/conftest.py diff --git a/imkar/scattering/__init__.py b/imkar/scattering/__init__.py index 5e3fc4d..0fff9c5 100644 --- a/imkar/scattering/__init__.py +++ b/imkar/scattering/__init__.py @@ -1,9 +1,9 @@ -from .coefficient import ( - random_incidence, +from .scattering import ( + random, freefield ) __all__ = [ 'freefield', - 'random_incidence' + 'random' ] diff --git a/imkar/scattering/coefficient.py b/imkar/scattering/scattering.py similarity index 73% rename from imkar/scattering/coefficient.py rename to imkar/scattering/scattering.py index 14e059f..cf93944 100644 --- a/imkar/scattering/coefficient.py +++ b/imkar/scattering/scattering.py @@ -27,11 +27,12 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): 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 `sample_pressure`. - weights_microphones : np.ndarray + reference sample. Needs to have the same cshape and frequencies as + `sample_pressure`. + microphone_weights : np.ndarray 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`. + Its shape needs to be (#microphones), so it matches the last dimension + in the cshape of `sample_pressure` and `reference_pressure`. Returns ------- @@ -49,19 +50,21 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): """ # check inputs if not isinstance(sample_pressure, pf.FrequencyData): - raise ValueError("sample_pressure has to be a pyfar.FrequencyData object") + 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(weights_microphones, np.ndarray): - raise ValueError("weights_microphones have to be a numpy.array") + 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 weights_microphones.shape[0] != sample_pressure.cshape[-1]: + if microphone_weights.shape[0] != sample_pressure.cshape[-1]: raise ValueError( "the last dimension of sample_pressure need be same as the " - "weights_microphones.shape.") + "microphone_weights.shape.") if not np.allclose( sample_pressure.frequencies, reference_pressure.frequencies): raise ValueError( @@ -75,9 +78,9 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): p_reference_sq = np.abs(p_reference)**2 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) + 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)) @@ -90,7 +93,7 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): return scattering_coefficients -def random_incidence( +def random( scattering_coefficients, incident_positions): r"""Calculate the random-incidence scattering coefficient according to Paris formula. @@ -100,18 +103,19 @@ def random_incidence( with the ``scattering_coefficients``, and the area weights ``w`` from the ``incident_positions``. - Note that the incident directions should be + 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 (..., #angle1, #angle2) + 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 (#angle1, #angle2). In - sperical coordinates the radii need to be constant. + 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 ------- @@ -122,10 +126,18 @@ def random_incidence( raise ValueError("scattering_coefficients has to be FrequencyData") if not isinstance(incident_positions, pf.Coordinates): raise ValueError("incident_positions have to be None or Coordinates") + if incident_positions.cshape[0] != scattering_coefficients.cshape[-1]: + raise ValueError( + "the last dimension of scattering_coefficients need be same as " + "the incident_positions.cshape.") theta = incident_positions.get_sph().T[1] weight = np.cos(theta) * incident_positions.weights norm = np.sum(weight) - random_scattering.freq = np.sum(scattering_coefficients.freq*weight/norm, axis=-2) - random_scattering.comment = 'random-incidence scattering coefficient' + coefficients = np.swapaxes(scattering_coefficients.freq, -1, -2) + random_scattering = pf.FrequencyData( + np.sum(coefficients*weight/norm, axis=-1), + scattering_coefficients.frequencies, + comment='random-incidence scattering coefficient' + ) return random_scattering diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5359ebf --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,17 @@ +import pytest +import pyfar as pf +import numpy as np + + +@pytest.fixture +def half_sphere_gaussian(): + """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] diff --git a/tests/test_scattering_coefficient.py b/tests/test_scattering_coefficient.py index 22c9717..c89394e 100644 --- a/tests/test_scattering_coefficient.py +++ b/tests/test_scattering_coefficient.py @@ -17,7 +17,7 @@ def test_freefield_1(frequencies): mics.cshape, 0, frequencies) p_sample.freq[5, :] = 0 p_reference.freq[5, :] = np.sum(p_sample.freq.flatten())/2 - s = scattering.coefficient.freefield(p_sample, p_reference, mics.weights) + s = scattering.freefield(p_sample, p_reference, mics.weights) np.testing.assert_allclose(s.freq, 1) @@ -33,20 +33,20 @@ def test_freefield_wrong_input(frequencies): p_sample.freq[5, :] = 0 p_reference.freq[5, :] = np.sum(p_sample.freq.flatten())/2 with pytest.raises(ValueError, match='sample_pressure'): - scattering.coefficient.freefield(1, p_reference, mics.weights) + scattering.freefield(1, p_reference, mics.weights) with pytest.raises(ValueError, match='reference_pressure'): - scattering.coefficient.freefield(p_sample, 1, mics.weights) - with pytest.raises(ValueError, match='weights_microphones'): - scattering.coefficient.freefield(p_sample, p_reference, 1) + scattering.freefield(p_sample, 1, mics.weights) + with pytest.raises(ValueError, match='microphone_weights'): + scattering.freefield(p_sample, p_reference, 1) with pytest.raises(ValueError, match='cshape'): - scattering.coefficient.freefield( + scattering.freefield( p_sample[:-2, ...], p_reference, mics.weights) - with pytest.raises(ValueError, match='weights_microphones'): - scattering.coefficient.freefield( + with pytest.raises(ValueError, match='microphone_weights'): + scattering.freefield( p_sample, p_reference, mics.weights[:10]) with pytest.raises(ValueError, match='same frequencies'): p_sample.frequencies[0] = 1 - scattering.coefficient.freefield(p_sample, p_reference, mics.weights) + scattering.freefield(p_sample, p_reference, mics.weights) @pytest.mark.parametrize("frequencies", [ @@ -61,7 +61,7 @@ def test_freefield_05(frequencies): p_sample.freq[7, :] = 1 p_sample.freq[28, :] = 1 p_reference.freq[7, :] = 1 - s = scattering.coefficient.freefield(p_sample, p_reference, mics.weights) + s = scattering.freefield(p_sample, p_reference, mics.weights) np.testing.assert_allclose(s.freq, 0.5) @@ -76,7 +76,7 @@ def test_freefield_0(frequencies): mics.cshape, 0, frequencies) p_reference.freq[5, :] = 1 p_sample.freq[5, :] = 1 - s = scattering.coefficient.freefield(p_sample, p_reference, mics.weights) + s = scattering.freefield(p_sample, p_reference, mics.weights) np.testing.assert_allclose(s.freq, 0) assert s.freq.shape[-1] == len(frequencies) @@ -99,8 +99,8 @@ def test_freefield_0_with_incident(frequencies): data_shape, 0, frequencies) p_reference.freq[:, 2, :] = 1 p_sample.freq[:, 2, :] = 1 - s = scattering.coefficient.freefield(p_sample, p_reference, mics.weights) - s_rand = scattering.coefficient.random_incidence(s, incident_directions) + s = scattering.freefield(p_sample, p_reference, mics.weights) + s_rand = scattering.random(s, incident_directions) np.testing.assert_allclose(s.freq, 0) np.testing.assert_allclose(s_rand.freq, 0) assert s.freq.shape[-1] == len(frequencies) @@ -127,8 +127,8 @@ def test_freefield_1_with_incidence( data_shape, 0, frequencies) p_reference.freq[:, 2, :] = 1 p_sample.freq[:, 3, :] = 1 - s = scattering.coefficient.freefield(p_sample, p_reference, mics.weights) - s_rand = scattering.coefficient.random_incidence(s, incident_directions) + s = scattering.freefield(p_sample, p_reference, mics.weights) + s_rand = scattering.random(s, incident_directions) np.testing.assert_allclose(s.freq, 1) np.testing.assert_allclose(s_rand.freq, 1) assert s.freq.shape[-1] == len(frequencies) @@ -157,8 +157,8 @@ def test_freefield_05_with_inci(frequencies, mics): p_sample.freq[:, 7, :] = 1 p_sample.freq[:, 5, :] = 1 p_reference.freq[:, 5, :] = 1 - s = scattering.coefficient.freefield(p_sample, p_reference, mics.weights) - s_rand = scattering.coefficient.random_incidence(s, incident_directions) + s = scattering.freefield(p_sample, p_reference, mics.weights) + s_rand = scattering.random(s, incident_directions) np.testing.assert_allclose(s.freq, 0.5) np.testing.assert_allclose(s_rand.freq, 0.5) assert s.freq.shape[-1] == len(frequencies) @@ -170,19 +170,19 @@ def test_freefield_05_with_inci(frequencies, mics): (0), (0.2), (0.5), (0.8), (1)]) @pytest.mark.parametrize("frequencies", [ ([100, 200]), ([100])]) -def test_random_incidence_constant_s( +def test_random_constant_s( s_value, frequencies): coords = pf.samplings.sph_gaussian(10) incident_directions = coords[coords.get_sph().T[1] <= np.pi/2] s = stub_utils.frequency_data_from_shape( incident_directions.cshape, s_value, frequencies) - s_rand = scattering.coefficient.random_incidence(s, incident_directions) + s_rand = scattering.random(s, incident_directions) np.testing.assert_allclose(s_rand.freq, s_value) @pytest.mark.parametrize("frequencies", [ ([100, 200]), ([100])]) -def test_random_incidence_non_constant_s(frequencies): +def test_random_non_constant_s(frequencies): data = pf.samplings.sph_gaussian(10) incident_directions = data[data.get_sph().T[1] <= np.pi/2] incident_cshape = incident_directions.cshape @@ -193,6 +193,6 @@ def test_random_incidence_non_constant_s(frequencies): actual_weight /= np.sum(actual_weight) s = stub_utils.frequency_data_from_shape( [], s_value, frequencies) - s_rand = scattering.coefficient.random_incidence(s, incident_directions) + s_rand = scattering.random(s, incident_directions) desired = np.sum(s_value*actual_weight) np.testing.assert_allclose(s_rand.freq, desired) From 112cf9066cdaee150777eb5a6cfa7307fa58d627 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 14 Apr 2023 13:59:08 +0200 Subject: [PATCH 10/32] better labeling --- imkar/scattering/scattering.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/imkar/scattering/scattering.py b/imkar/scattering/scattering.py index cf93944..3babf07 100644 --- a/imkar/scattering/scattering.py +++ b/imkar/scattering/scattering.py @@ -94,7 +94,7 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): def random( - scattering_coefficients, incident_positions): + scattering_coefficients, incident_directions): r"""Calculate the random-incidence scattering coefficient according to Paris formula. @@ -102,7 +102,7 @@ def random( 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_positions``. + area weights ``w`` from the ``incident_directions``. Note that the incident directions should be equally distributed to get a valid result. @@ -124,15 +124,15 @@ def random( """ if not isinstance(scattering_coefficients, pf.FrequencyData): raise ValueError("scattering_coefficients has to be FrequencyData") - if not isinstance(incident_positions, pf.Coordinates): - raise ValueError("incident_positions have to be None or Coordinates") - if incident_positions.cshape[0] != scattering_coefficients.cshape[-1]: + if not isinstance(incident_directions, pf.Coordinates): + raise ValueError("incident_directions have to be None or Coordinates") + if incident_directions.cshape[0] != scattering_coefficients.cshape[-1]: raise ValueError( "the last dimension of scattering_coefficients need be same as " - "the incident_positions.cshape.") + "the incident_directions.cshape.") - theta = incident_positions.get_sph().T[1] - weight = np.cos(theta) * incident_positions.weights + theta = incident_directions.get_sph().T[1] + weight = np.cos(theta) * incident_directions.weights norm = np.sum(weight) coefficients = np.swapaxes(scattering_coefficients.freq, -1, -2) random_scattering = pf.FrequencyData( From 0baf823f6357cf6171ae96ec3475ca797bd29924 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Sun, 16 Apr 2023 19:01:22 +0200 Subject: [PATCH 11/32] fix new structure in docs --- docs/modules.rst | 2 +- docs/modules/imkar.scattering.coefficient.rst | 7 ------- docs/modules/imkar.scattering.rst | 7 +++++++ 3 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 docs/modules/imkar.scattering.coefficient.rst create mode 100644 docs/modules/imkar.scattering.rst diff --git a/docs/modules.rst b/docs/modules.rst index 9da889f..b041bb9 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -7,4 +7,4 @@ according to their modules. .. toctree:: :maxdepth: 1 - modules/imkar.scattering.coefficient + modules/imkar.scattering diff --git a/docs/modules/imkar.scattering.coefficient.rst b/docs/modules/imkar.scattering.coefficient.rst deleted file mode 100644 index f384e68..0000000 --- a/docs/modules/imkar.scattering.coefficient.rst +++ /dev/null @@ -1,7 +0,0 @@ -imkar.scattering.coefficient -============================ - -.. automodule:: imkar.scattering.coefficient - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/modules/imkar.scattering.rst b/docs/modules/imkar.scattering.rst new file mode 100644 index 0000000..69bb608 --- /dev/null +++ b/docs/modules/imkar.scattering.rst @@ -0,0 +1,7 @@ +imkar.scattering +================ + +.. automodule:: imkar.scattering + :members: + :undoc-members: + :show-inheritance: From 00f76b140245f619c0376ea0ed9f5d0ff2b382ff Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Sun, 16 Apr 2023 19:58:10 +0200 Subject: [PATCH 12/32] should not be part of this pr --- tests/test_imkar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_imkar.py b/tests/test_imkar.py index 6b6c622..f801097 100644 --- a/tests/test_imkar.py +++ b/tests/test_imkar.py @@ -5,7 +5,7 @@ import pytest -from imkar import imkar # noqa +from imkar import imkar @pytest.fixture @@ -18,7 +18,7 @@ def response(): # return requests.get('https://github.com/mberz/cookiecutter-pypackage') -def test_content(response): # noqa +def test_content(response): """Sample pytest test function with the pytest fixture as an argument.""" # from bs4 import BeautifulSoup # assert 'GitHub' in BeautifulSoup(response.content).title.string From b29353fbe9db801f79bfe1103bb55ea46966abe5 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Sun, 16 Apr 2023 20:52:37 +0200 Subject: [PATCH 13/32] remove stubs as proposed by @sikersten --- imkar/testing/__init__.py | 7 - imkar/testing/stub_utils.py | 22 --- tests/conftest.py | 64 ++++++++- tests/test_scattering.py | 147 ++++++++++++++++++++ tests/test_scattering_coefficient.py | 198 --------------------------- tests/test_sub_utils.py | 37 ----- 6 files changed, 210 insertions(+), 265 deletions(-) delete mode 100644 imkar/testing/__init__.py delete mode 100644 imkar/testing/stub_utils.py create mode 100644 tests/test_scattering.py delete mode 100644 tests/test_scattering_coefficient.py delete mode 100644 tests/test_sub_utils.py diff --git a/imkar/testing/__init__.py b/imkar/testing/__init__.py deleted file mode 100644 index dc61984..0000000 --- a/imkar/testing/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .stub_utils import ( - frequency_data_from_shape, -) - -__all__ = [ - 'frequency_data_from_shape', - ] diff --git a/imkar/testing/stub_utils.py b/imkar/testing/stub_utils.py deleted file mode 100644 index 3375398..0000000 --- a/imkar/testing/stub_utils.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -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 diff --git a/tests/conftest.py b/tests/conftest.py index 5359ebf..a98287c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ @pytest.fixture -def half_sphere_gaussian(): +def half_sphere(): """return 42th order gaussian sampling for the half sphere and radius 1. Returns @@ -15,3 +15,65 @@ def half_sphere_gaussian(): 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) diff --git a/tests/test_scattering.py b/tests/test_scattering.py new file mode 100644 index 0000000..5be16fd --- /dev/null +++ b/tests/test_scattering.py @@ -0,0 +1,147 @@ +import pytest +import numpy as np +import pyfar as pf + +from imkar import scattering + + +def test_freefield_1( + half_sphere, pressure_data_mics): + mics = half_sphere + p_sample = pressure_data_mics.copy() + p_sample.freq.fill(1) + p_reference = pressure_data_mics.copy() + p_sample.freq[5, :] = 0 + p_reference.freq[5, :] = np.sum(p_sample.freq.flatten())/2 + s = scattering.freefield(p_sample, p_reference, mics.weights) + np.testing.assert_allclose(s.freq, 1) + + +def test_freefield_wrong_input( + half_sphere, pressure_data_mics): + mics = half_sphere + p_sample = pressure_data_mics.copy() + p_reference = pressure_data_mics.copy() + + with pytest.raises(ValueError, match='sample_pressure'): + scattering.freefield(1, p_reference, mics.weights) + with pytest.raises(ValueError, match='reference_pressure'): + scattering.freefield(p_sample, 1, mics.weights) + with pytest.raises(ValueError, match='microphone_weights'): + scattering.freefield(p_sample, p_reference, 1) + with pytest.raises(ValueError, match='cshape'): + scattering.freefield( + p_sample[:-2, ...], p_reference, mics.weights) + with pytest.raises(ValueError, match='microphone_weights'): + scattering.freefield( + p_sample, p_reference, mics.weights[:10]) + with pytest.raises(ValueError, match='same frequencies'): + p_sample.frequencies[0] = 1 + scattering.freefield(p_sample, p_reference, mics.weights) + + +def test_freefield_05( + half_sphere, pressure_data_mics): + mics = half_sphere + p_sample = pressure_data_mics.copy() + p_reference = pressure_data_mics.copy() + p_sample.freq[7, :] = 1 + p_sample.freq[28, :] = 1 + p_reference.freq[7, :] = 1 + s = scattering.freefield(p_sample, p_reference, mics.weights) + np.testing.assert_allclose(s.freq, 0.5) + + +def test_freefield_0( + half_sphere, pressure_data_mics): + mics = half_sphere + p_sample = pressure_data_mics.copy() + p_reference = pressure_data_mics.copy() + p_reference.freq[5, :] = 1 + p_sample.freq[5, :] = 1 + s = scattering.freefield(p_sample, p_reference, mics.weights) + np.testing.assert_allclose(s.freq, 0) + assert s.freq.shape[-1] == p_sample.n_bins + + +def test_freefield_0_with_incident( + half_sphere, quarter_half_sphere, + pressure_data_mics_incident_directions): + mics = half_sphere + incident_directions = quarter_half_sphere + p_sample = pressure_data_mics_incident_directions.copy() + p_reference = pressure_data_mics_incident_directions.copy() + p_reference.freq[:, 2, :] = 1 + p_sample.freq[:, 2, :] = 1 + s = scattering.freefield(p_sample, p_reference, mics.weights) + s_rand = scattering.random(s, incident_directions) + np.testing.assert_allclose(s.freq, 0) + np.testing.assert_allclose(s_rand.freq, 0) + assert s.freq.shape[-1] == p_sample.n_bins + assert s.cshape == incident_directions.cshape + assert s_rand.freq.shape[-1] == p_sample.n_bins + + +def test_freefield_1_with_incidence( + half_sphere, quarter_half_sphere, + pressure_data_mics_incident_directions): + mics = half_sphere + incident_directions = quarter_half_sphere + p_sample = pressure_data_mics_incident_directions.copy() + p_reference = pressure_data_mics_incident_directions.copy() + p_reference.freq[:, 2, :] = 1 + p_sample.freq[:, 3, :] = 1 + s = scattering.freefield(p_sample, p_reference, mics.weights) + s_rand = scattering.random(s, incident_directions) + np.testing.assert_allclose(s.freq, 1) + np.testing.assert_allclose(s_rand.freq, 1) + assert s.freq.shape[-1] == p_sample.n_bins + assert s.cshape == incident_directions.cshape + assert s_rand.freq.shape[-1] == p_sample.n_bins + + +def test_freefield_05_with_inci( + half_sphere, quarter_half_sphere, + pressure_data_mics_incident_directions): + mics = half_sphere + incident_directions = quarter_half_sphere + p_sample = pressure_data_mics_incident_directions.copy() + p_reference = pressure_data_mics_incident_directions.copy() + p_sample.freq[:, 7, :] = 1 + p_sample.freq[:, 28, :] = 1 + p_reference.freq[:, 7, :] = 1 + s = scattering.freefield(p_sample, p_reference, mics.weights) + s_rand = scattering.random(s, incident_directions) + np.testing.assert_allclose(s.freq, 0.5) + np.testing.assert_allclose(s_rand.freq, 0.5) + assert s.freq.shape[-1] == p_sample.n_bins + assert s.cshape == incident_directions.cshape + assert s_rand.freq.shape[-1] == p_sample.n_bins + + +@pytest.mark.parametrize("s_value", [ + (0), (0.2), (0.5), (0.8), (1)]) +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +def test_random_constant_s( + s_value, frequencies, half_sphere): + incident_directions = half_sphere + shape = np.append(half_sphere.cshape, len(frequencies)) + s = pf.FrequencyData(np.zeros(shape)+s_value, frequencies) + s_rand = scattering.random(s, incident_directions) + np.testing.assert_allclose(s_rand.freq, s_value) + + +def test_random_non_constant_s(): + data = pf.samplings.sph_gaussian(10) + incident_directions = data[data.get_sph().T[1] <= np.pi/2] + incident_cshape = incident_directions.cshape + s_value = np.arange( + incident_cshape[0]).reshape(incident_cshape) / incident_cshape[0] + theta = incident_directions.get_sph().T[1] + actual_weight = np.cos(theta) * incident_directions.weights + actual_weight /= np.sum(actual_weight) + s = pf.FrequencyData(s_value.reshape((50, 1)), [100]) + s_rand = scattering.random(s, incident_directions) + desired = np.sum(s_value*actual_weight) + np.testing.assert_allclose(s_rand.freq, desired) diff --git a/tests/test_scattering_coefficient.py b/tests/test_scattering_coefficient.py deleted file mode 100644 index c89394e..0000000 --- a/tests/test_scattering_coefficient.py +++ /dev/null @@ -1,198 +0,0 @@ -import pytest -import numpy as np -import pyfar as pf - -from imkar import scattering -from imkar.testing import stub_utils - - -@pytest.mark.parametrize("frequencies", [ - ([100, 200]), ([100])]) -def test_freefield_1(frequencies): - mics = pf.samplings.sph_gaussian(42) - mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere - p_sample = stub_utils.frequency_data_from_shape( - mics.cshape, 1, frequencies) - p_reference = stub_utils.frequency_data_from_shape( - mics.cshape, 0, frequencies) - p_sample.freq[5, :] = 0 - p_reference.freq[5, :] = np.sum(p_sample.freq.flatten())/2 - s = scattering.freefield(p_sample, p_reference, mics.weights) - np.testing.assert_allclose(s.freq, 1) - - -@pytest.mark.parametrize("frequencies", [ - ([100, 200]), ([100])]) -def test_freefield_wrong_input(frequencies): - mics = pf.samplings.sph_gaussian(42) - mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere - p_sample = stub_utils.frequency_data_from_shape( - mics.cshape, 1, frequencies) - p_reference = stub_utils.frequency_data_from_shape( - mics.cshape, 0, frequencies) - p_sample.freq[5, :] = 0 - p_reference.freq[5, :] = np.sum(p_sample.freq.flatten())/2 - with pytest.raises(ValueError, match='sample_pressure'): - scattering.freefield(1, p_reference, mics.weights) - with pytest.raises(ValueError, match='reference_pressure'): - scattering.freefield(p_sample, 1, mics.weights) - with pytest.raises(ValueError, match='microphone_weights'): - scattering.freefield(p_sample, p_reference, 1) - with pytest.raises(ValueError, match='cshape'): - scattering.freefield( - p_sample[:-2, ...], p_reference, mics.weights) - with pytest.raises(ValueError, match='microphone_weights'): - scattering.freefield( - p_sample, p_reference, mics.weights[:10]) - with pytest.raises(ValueError, match='same frequencies'): - p_sample.frequencies[0] = 1 - scattering.freefield(p_sample, p_reference, mics.weights) - - -@pytest.mark.parametrize("frequencies", [ - ([100, 200]), ([100])]) -def test_freefield_05(frequencies): - mics = pf.samplings.sph_gaussian(42) - mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere - p_sample = stub_utils.frequency_data_from_shape( - mics.cshape, 0, frequencies) - p_reference = stub_utils.frequency_data_from_shape( - mics.cshape, 0, frequencies) - p_sample.freq[7, :] = 1 - p_sample.freq[28, :] = 1 - p_reference.freq[7, :] = 1 - s = scattering.freefield(p_sample, p_reference, mics.weights) - np.testing.assert_allclose(s.freq, 0.5) - - -@pytest.mark.parametrize("frequencies", [ - ([100, 200]), ([100])]) -def test_freefield_0(frequencies): - mics = pf.samplings.sph_gaussian(42) - mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere - p_sample = stub_utils.frequency_data_from_shape( - mics.cshape, 0, frequencies) - p_reference = stub_utils.frequency_data_from_shape( - mics.cshape, 0, frequencies) - p_reference.freq[5, :] = 1 - p_sample.freq[5, :] = 1 - s = scattering.freefield(p_sample, p_reference, mics.weights) - np.testing.assert_allclose(s.freq, 0) - assert s.freq.shape[-1] == len(frequencies) - - -@pytest.mark.parametrize("frequencies", [ - ([100, 200]), ([100])]) -def test_freefield_0_with_incident(frequencies): - mics = pf.samplings.sph_equal_area(42) - mics.weights = pf.samplings.calculate_sph_voronoi_weights(mics) - mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere - incident_directions = pf.samplings.sph_gaussian(10) - incident_directions = incident_directions[ - incident_directions.get_sph().T[1] <= np.pi/2] - incident_directions = incident_directions[ - incident_directions.get_sph().T[0] <= np.pi/2] - data_shape = np.array((incident_directions.cshape, mics.cshape)).flatten() - p_sample = stub_utils.frequency_data_from_shape( - data_shape, 0, frequencies) - p_reference = stub_utils.frequency_data_from_shape( - data_shape, 0, frequencies) - p_reference.freq[:, 2, :] = 1 - p_sample.freq[:, 2, :] = 1 - s = scattering.freefield(p_sample, p_reference, mics.weights) - s_rand = scattering.random(s, incident_directions) - np.testing.assert_allclose(s.freq, 0) - np.testing.assert_allclose(s_rand.freq, 0) - assert s.freq.shape[-1] == len(frequencies) - assert s.cshape == incident_directions.cshape - assert s_rand.freq.shape[-1] == len(frequencies) - - -@pytest.mark.parametrize("frequencies", [ - ([100, 200]), ([100])]) -def test_freefield_1_with_incidence( - frequencies): - mics = pf.samplings.sph_equal_angle(10) - mics.weights = pf.samplings.calculate_sph_voronoi_weights(mics) - mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere - incident_directions = pf.samplings.sph_gaussian(10) - incident_directions = incident_directions[ - incident_directions.get_sph().T[1] <= np.pi/2] - incident_directions = incident_directions[ - incident_directions.get_sph().T[0] <= np.pi/2] - data_shape = np.array((incident_directions.cshape, mics.cshape)).flatten() - p_sample = stub_utils.frequency_data_from_shape( - data_shape, 0, frequencies) - p_reference = stub_utils.frequency_data_from_shape( - data_shape, 0, frequencies) - p_reference.freq[:, 2, :] = 1 - p_sample.freq[:, 3, :] = 1 - s = scattering.freefield(p_sample, p_reference, mics.weights) - s_rand = scattering.random(s, incident_directions) - np.testing.assert_allclose(s.freq, 1) - np.testing.assert_allclose(s_rand.freq, 1) - assert s.freq.shape[-1] == len(frequencies) - assert s.cshape == incident_directions.cshape - assert s_rand.freq.shape[-1] == len(frequencies) - - -@pytest.mark.parametrize("frequencies", [ - ([100, 200]), ([100])]) -@pytest.mark.parametrize("mics", [ - (pf.samplings.sph_equal_angle(10)), - ]) -def test_freefield_05_with_inci(frequencies, mics): - mics.weights = pf.samplings.calculate_sph_voronoi_weights(mics) - mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere - incident_directions = pf.samplings.sph_gaussian(10) - incident_directions = incident_directions[ - incident_directions.get_sph().T[1] <= np.pi/2] - incident_directions = incident_directions[ - incident_directions.get_sph().T[0] <= np.pi/2] - data_shape = np.array((incident_directions.cshape, mics.cshape)).flatten() - p_sample = stub_utils.frequency_data_from_shape( - data_shape, 0, frequencies) - p_reference = stub_utils.frequency_data_from_shape( - data_shape, 0, frequencies) - p_sample.freq[:, 7, :] = 1 - p_sample.freq[:, 5, :] = 1 - p_reference.freq[:, 5, :] = 1 - s = scattering.freefield(p_sample, p_reference, mics.weights) - s_rand = scattering.random(s, incident_directions) - np.testing.assert_allclose(s.freq, 0.5) - np.testing.assert_allclose(s_rand.freq, 0.5) - assert s.freq.shape[-1] == len(frequencies) - assert s.cshape == incident_directions.cshape - assert s_rand.freq.shape[-1] == len(frequencies) - - -@pytest.mark.parametrize("s_value", [ - (0), (0.2), (0.5), (0.8), (1)]) -@pytest.mark.parametrize("frequencies", [ - ([100, 200]), ([100])]) -def test_random_constant_s( - s_value, frequencies): - coords = pf.samplings.sph_gaussian(10) - incident_directions = coords[coords.get_sph().T[1] <= np.pi/2] - s = stub_utils.frequency_data_from_shape( - incident_directions.cshape, s_value, frequencies) - s_rand = scattering.random(s, incident_directions) - np.testing.assert_allclose(s_rand.freq, s_value) - - -@pytest.mark.parametrize("frequencies", [ - ([100, 200]), ([100])]) -def test_random_non_constant_s(frequencies): - data = pf.samplings.sph_gaussian(10) - incident_directions = data[data.get_sph().T[1] <= np.pi/2] - incident_cshape = incident_directions.cshape - s_value = np.arange( - incident_cshape[0]).reshape(incident_cshape) / incident_cshape[0] - theta = incident_directions.get_sph().T[1] - actual_weight = np.cos(theta) * incident_directions.weights - actual_weight /= np.sum(actual_weight) - s = stub_utils.frequency_data_from_shape( - [], s_value, frequencies) - s_rand = scattering.random(s, incident_directions) - desired = np.sum(s_value*actual_weight) - np.testing.assert_allclose(s_rand.freq, desired) diff --git a/tests/test_sub_utils.py b/tests/test_sub_utils.py deleted file mode 100644 index 99a9003..0000000 --- a/tests/test_sub_utils.py +++ /dev/null @@ -1,37 +0,0 @@ -import pytest -import numpy as np - -from imkar.testing import stub_utils - - -@pytest.mark.parametrize( - "shapes", [ - (3, 2), - (5, 2), - (3, 2, 7), - ]) -@pytest.mark.parametrize( - "data_in", [ - 0.1, - 0, - np.array([0.1, 1]), - np.arange(4*5).reshape(4, 5), - ]) -@pytest.mark.parametrize( - "frequency", [ - [100], - [100, 200], - ]) -def test_frequency_data_from_shape(shapes, data_in, frequency): - data = stub_utils.frequency_data_from_shape(shapes, data_in, frequency) - # npt.assert_allclose(data.freq, data_in) - if hasattr(data_in, '__len__'): - for idx in range(len(data_in.shape)): - assert data.cshape[idx] == data_in.shape[idx] - for idx in range(len(shapes)): - assert data.cshape[idx+len(data_in.shape)] == shapes[idx] - - else: - for idx in range(len(shapes)): - assert data.cshape[idx] == shapes[idx] - assert data.n_bins == len(frequency) From 5c7408e3595012aaa1c2859ae2951f26c1ddbeee Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Sun, 16 Apr 2023 21:03:13 +0200 Subject: [PATCH 14/32] separate paris formula in utils as discussed --- imkar/__init__.py | 1 + imkar/scattering/__init__.py | 4 +-- imkar/scattering/scattering.py | 22 +++------------ imkar/utils/__init__.py | 7 +++++ imkar/utils/utils.py | 51 ++++++++++++++++++++++++++++++++++ tests/test_scattering.py | 16 ++--------- tests/test_utils.py | 34 +++++++++++++++++++++++ 7 files changed, 101 insertions(+), 34 deletions(-) create mode 100644 imkar/utils/__init__.py create mode 100644 imkar/utils/utils.py create mode 100644 tests/test_utils.py diff --git a/imkar/__init__.py b/imkar/__init__.py index e176307..d221da6 100644 --- a/imkar/__init__.py +++ b/imkar/__init__.py @@ -6,3 +6,4 @@ from . import scattering # noqa +from . import utils # noqa diff --git a/imkar/scattering/__init__.py b/imkar/scattering/__init__.py index 0fff9c5..96a3704 100644 --- a/imkar/scattering/__init__.py +++ b/imkar/scattering/__init__.py @@ -1,9 +1,9 @@ from .scattering import ( + freefield, random, - freefield ) __all__ = [ 'freefield', - 'random' + 'random', ] diff --git a/imkar/scattering/scattering.py b/imkar/scattering/scattering.py index 3babf07..1a967f8 100644 --- a/imkar/scattering/scattering.py +++ b/imkar/scattering/scattering.py @@ -1,5 +1,6 @@ import numpy as np import pyfar as pf +from imkar import utils def freefield(sample_pressure, reference_pressure, microphone_weights): @@ -122,22 +123,7 @@ def random( random_scattering : pyfar.FrequencyData The random-incidence scattering coefficient. """ - if not isinstance(scattering_coefficients, pf.FrequencyData): - raise ValueError("scattering_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] != scattering_coefficients.cshape[-1]: - raise ValueError( - "the last dimension of scattering_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 = np.swapaxes(scattering_coefficients.freq, -1, -2) - random_scattering = pf.FrequencyData( - np.sum(coefficients*weight/norm, axis=-1), - scattering_coefficients.frequencies, - comment='random-incidence scattering coefficient' - ) + random_scattering = utils.paris_formula( + scattering_coefficients, incident_directions) + random_scattering.comment = 'random-incidence scattering coefficient' return random_scattering diff --git a/imkar/utils/__init__.py b/imkar/utils/__init__.py new file mode 100644 index 0000000..eaf3a04 --- /dev/null +++ b/imkar/utils/__init__.py @@ -0,0 +1,7 @@ +from .utils import ( + paris_formula, +) + +__all__ = [ + 'paris_formula', + ] diff --git a/imkar/utils/utils.py b/imkar/utils/utils.py new file mode 100644 index 0000000..39069ec --- /dev/null +++ b/imkar/utils/utils.py @@ -0,0 +1,51 @@ +import numpy as np +import pyfar as pf + + +def paris_formula(coefficients, incident_directions): + 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) + incident_directions : pyfar.Coordinates + Defines the incidence directions of each `coefficients` in a + Coordinates object. Its cshape need to be (#incident_directions). In + 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. + """ + 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 diff --git a/tests/test_scattering.py b/tests/test_scattering.py index 5be16fd..8d1fe7f 100644 --- a/tests/test_scattering.py +++ b/tests/test_scattering.py @@ -123,25 +123,13 @@ def test_freefield_05_with_inci( (0), (0.2), (0.5), (0.8), (1)]) @pytest.mark.parametrize("frequencies", [ ([100, 200]), ([100])]) -def test_random_constant_s( +def test_random_comment( s_value, frequencies, half_sphere): incident_directions = half_sphere shape = np.append(half_sphere.cshape, len(frequencies)) s = pf.FrequencyData(np.zeros(shape)+s_value, frequencies) s_rand = scattering.random(s, incident_directions) np.testing.assert_allclose(s_rand.freq, s_value) + assert s_rand.comment == 'random-incidence scattering coefficient' -def test_random_non_constant_s(): - data = pf.samplings.sph_gaussian(10) - incident_directions = data[data.get_sph().T[1] <= np.pi/2] - incident_cshape = incident_directions.cshape - s_value = np.arange( - incident_cshape[0]).reshape(incident_cshape) / incident_cshape[0] - theta = incident_directions.get_sph().T[1] - actual_weight = np.cos(theta) * incident_directions.weights - actual_weight /= np.sum(actual_weight) - s = pf.FrequencyData(s_value.reshape((50, 1)), [100]) - s_rand = scattering.random(s, incident_directions) - desired = np.sum(s_value*actual_weight) - np.testing.assert_allclose(s_rand.freq, desired) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..1440c0b --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,34 @@ +import pytest +import numpy as np +import pyfar as pf + +from imkar import utils + + +@pytest.mark.parametrize("c_value", [ + (0), (0.2), (0.5), (0.8), (1)]) +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +def test_random_constant_coefficient( + c_value, frequencies, half_sphere): + incident_directions = half_sphere + shape = np.append(half_sphere.cshape, len(frequencies)) + coefficient = pf.FrequencyData(np.zeros(shape)+c_value, frequencies) + c_rand = utils.paris_formula(coefficient, incident_directions) + np.testing.assert_allclose(c_rand.freq, c_value) + assert c_rand.comment == 'random-incidence coefficient' + + +def test_random_non_constant_coefficient(): + data = pf.samplings.sph_gaussian(10) + incident_directions = data[data.get_sph().T[1] <= np.pi/2] + incident_cshape = incident_directions.cshape + s_value = np.arange( + incident_cshape[0]).reshape(incident_cshape) / incident_cshape[0] + theta = incident_directions.get_sph().T[1] + actual_weight = np.cos(theta) * incident_directions.weights + actual_weight /= np.sum(actual_weight) + coefficient = pf.FrequencyData(s_value.reshape((50, 1)), [100]) + c_rand = utils.paris_formula(coefficient, incident_directions) + desired = np.sum(s_value*actual_weight) + np.testing.assert_allclose(c_rand.freq, desired) From 0987a4a861cd7d9421c44c257772692d524858ca Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Sun, 16 Apr 2023 21:03:30 +0200 Subject: [PATCH 15/32] flake8 --- tests/test_scattering.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_scattering.py b/tests/test_scattering.py index 8d1fe7f..7c26aed 100644 --- a/tests/test_scattering.py +++ b/tests/test_scattering.py @@ -131,5 +131,3 @@ def test_random_comment( s_rand = scattering.random(s, incident_directions) np.testing.assert_allclose(s_rand.freq, s_value) assert s_rand.comment == 'random-incidence scattering coefficient' - - From bb336886643f78329cd59aad0f6cfeb66ad6bc8d Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Tue, 23 May 2023 17:58:30 -0400 Subject: [PATCH 16/32] Update imkar/utils/utils.py Co-authored-by: Pascal Palenda --- imkar/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imkar/utils/utils.py b/imkar/utils/utils.py index 39069ec..e504b06 100644 --- a/imkar/utils/utils.py +++ b/imkar/utils/utils.py @@ -18,7 +18,7 @@ def paris_formula(coefficients, incident_directions): ---------- coefficients : pyfar.FrequencyData coefficients for different incident directions. Its cshape - need to be (..., #incident_directions) + needs to be (..., #incident_directions) incident_directions : pyfar.Coordinates Defines the incidence directions of each `coefficients` in a Coordinates object. Its cshape need to be (#incident_directions). In From d0544c9017b6dcd941204f7cfaf6a2fd68f14317 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Tue, 23 May 2023 17:58:43 -0400 Subject: [PATCH 17/32] Update imkar/utils/utils.py Co-authored-by: Pascal Palenda --- imkar/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imkar/utils/utils.py b/imkar/utils/utils.py index e504b06..ca3be5c 100644 --- a/imkar/utils/utils.py +++ b/imkar/utils/utils.py @@ -21,7 +21,7 @@ def paris_formula(coefficients, incident_directions): needs to be (..., #incident_directions) incident_directions : pyfar.Coordinates Defines the incidence directions of each `coefficients` in a - Coordinates object. Its cshape need to be (#incident_directions). In + Coordinates object. Its cshape needs to be (#incident_directions). In sperical coordinates the radii need to be constant. The weights need to reflect the area weights. From 87c48bf0cd2408089758c9c671fd26155ac2d66d Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 23 May 2023 18:34:08 -0400 Subject: [PATCH 18/32] add comments @pingelit --- imkar/scattering/scattering.py | 17 +++++++++++------ imkar/utils/utils.py | 11 ++++++++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/imkar/scattering/scattering.py b/imkar/scattering/scattering.py index 1a967f8..2b16a23 100644 --- a/imkar/scattering/scattering.py +++ b/imkar/scattering/scattering.py @@ -25,7 +25,7 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): ---------- sample_pressure : pyfar.FrequencyData Reflected sound pressure or directivity of the test sample. Its cshape - need to be (..., #microphones). + needs 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 @@ -64,7 +64,7 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): "same cshape.") if microphone_weights.shape[0] != sample_pressure.cshape[-1]: raise ValueError( - "the last dimension of sample_pressure need be same as the " + "the last dimension of sample_pressure needs be same as the " "microphone_weights.shape.") if not np.allclose( sample_pressure.frequencies, reference_pressure.frequencies): @@ -97,7 +97,7 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): def random( scattering_coefficients, incident_directions): r"""Calculate the random-incidence scattering coefficient - according to Paris formula. + according to Paris formula [2]_. .. math:: s_{rand} = \sum s(\vartheta_S,\varphi_S) \cdot cos(\vartheta_S) \cdot w @@ -111,17 +111,22 @@ def random( ---------- scattering_coefficients : pyfar.FrequencyData Scattering coefficients for different incident directions. Its cshape - need to be (..., #source_directions) + needs 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 + Coordinates object. Its cshape needs to be (#source_directions). In + sperical coordinates the radii needs to be constant. The weights need to reflect the area weights. Returns ------- random_scattering : pyfar.FrequencyData The random-incidence scattering coefficient. + + 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) diff --git a/imkar/utils/utils.py b/imkar/utils/utils.py index ca3be5c..cf4ff65 100644 --- a/imkar/utils/utils.py +++ b/imkar/utils/utils.py @@ -4,7 +4,7 @@ def paris_formula(coefficients, incident_directions): r"""Calculate the random-incidence coefficient - according to Paris formula. + according to Paris formula [2]_. .. math:: c_{rand} = \sum c(\vartheta_S,\varphi_S) \cdot cos(\vartheta_S) \cdot w @@ -22,13 +22,18 @@ def paris_formula(coefficients, incident_directions): incident_directions : pyfar.Coordinates Defines the incidence directions of each `coefficients` in a Coordinates object. Its cshape needs to be (#incident_directions). In - sperical coordinates the radii need to be constant. The weights need + sperical coordinates the radii needs to be constant. The weights need to reflect the area weights. Returns ------- random_coefficient : pyfar.FrequencyData The random-incidence scattering coefficient. + + 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") @@ -36,7 +41,7 @@ def paris_formula(coefficients, incident_directions): 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 last dimension of coefficients needs be same as " "the incident_directions.cshape.") theta = incident_directions.get_sph().T[1] From ada05f84401dca3537091d2f7354e4e78eb4012e Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 23 May 2023 18:35:02 -0400 Subject: [PATCH 19/32] cosmetics --- imkar/scattering/scattering.py | 3 ++- imkar/utils/utils.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/imkar/scattering/scattering.py b/imkar/scattering/scattering.py index 2b16a23..355c64d 100644 --- a/imkar/scattering/scattering.py +++ b/imkar/scattering/scattering.py @@ -96,7 +96,8 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): def random( scattering_coefficients, incident_directions): - r"""Calculate the random-incidence scattering coefficient + r""" + Calculate the random-incidence scattering coefficient according to Paris formula [2]_. .. math:: diff --git a/imkar/utils/utils.py b/imkar/utils/utils.py index cf4ff65..6eb8697 100644 --- a/imkar/utils/utils.py +++ b/imkar/utils/utils.py @@ -3,7 +3,8 @@ def paris_formula(coefficients, incident_directions): - r"""Calculate the random-incidence coefficient + r""" + Calculate the random-incidence coefficient according to Paris formula [2]_. .. math:: From df557533190072f50f8c80013357bd34edd2ab7f Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Mon, 12 Jun 2023 09:38:03 +0200 Subject: [PATCH 20/32] add review @f-brinkmann --- imkar/scattering/__init__.py | 5 +++ imkar/scattering/scattering.py | 75 +++++++++++++++++++--------------- imkar/utils/utils.py | 28 +++++++------ setup.py | 5 ++- 4 files changed, 67 insertions(+), 46 deletions(-) diff --git a/imkar/scattering/__init__.py b/imkar/scattering/__init__.py index 96a3704..b59d748 100644 --- a/imkar/scattering/__init__.py +++ b/imkar/scattering/__init__.py @@ -1,3 +1,8 @@ +""" +This module collects the functionality around sound scattering, such as, +(random incident) scattering coefficients, and analytical solutions. +""" + from .scattering import ( freefield, random, diff --git a/imkar/scattering/scattering.py b/imkar/scattering/scattering.py index 355c64d..7648922 100644 --- a/imkar/scattering/scattering.py +++ b/imkar/scattering/scattering.py @@ -5,40 +5,47 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): r""" - Calculate the free-field scattering coefficient for each incident direction - using the Mommertz correlation method [1]_: + 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(\vartheta_S,\varphi_S) = 1 - - \frac{|\sum \underline{p}_{sample}(\vartheta_R,\varphi_R) \cdot - \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 - :py:func:`random_incidence` to calculate the random incidence + 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 incidence angle and azimuth angles. See + :py:func:`random` to calculate the random incidence scattering coefficient. Parameters ---------- sample_pressure : pyfar.FrequencyData Reflected sound pressure or directivity of the test sample. Its cshape - needs to be (..., #microphones). + needs to be (..., microphone_weights.csize). reference_pressure : pyfar.FrequencyData Reflected sound pressure or directivity of the reference sample. Needs to have the same cshape and frequencies as - `sample_pressure`. + ``sample_pressure``. microphone_weights : np.ndarray 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`. + Its shape needs to match the last dimension in the cshape of + ``sample_pressure`` and ``reference_pressure``. Returns ------- scattering_coefficients : pyfar.FrequencyData - The scattering coefficient for each incident direction. + The scattering coefficient for each incident direction depending on + frequency. References @@ -56,8 +63,7 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): 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") + microphone_weights = np.asarray(microphone_weights) if sample_pressure.cshape != reference_pressure.cshape: raise ValueError( "sample_pressure and reference_pressure have to have the " @@ -89,7 +95,6 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): scattering_coefficients = pf.FrequencyData( np.moveaxis(data_scattering_coefficient, 0, -1), sample_pressure.frequencies) - scattering_coefficients.comment = 'scattering coefficient' return scattering_coefficients @@ -97,32 +102,37 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): def random( scattering_coefficients, incident_directions): r""" - Calculate the random-incidence scattering coefficient - according to Paris formula [2]_. + 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_S,\varphi_S) \cdot cos(\vartheta_S) \cdot w + s_{rand} = \sum s(\vartheta,\varphi) \cdot cos(\vartheta) \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. + with the scattering coefficients :math:`s(\vartheta,\varphi)`, the area + weights ``w`` from the ``incident_directions.weights``, + and :math:`\vartheta` and :math:`\varphi` are the incidence + angle and azimuth angles. 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 : pyfar.FrequencyData Scattering coefficients for different incident directions. Its cshape - needs to be (..., #source_directions) + needs to be (..., incident_directions.csize) incident_directions : pyfar.Coordinates Defines the incidence directions of each `scattering_coefficients` in a - Coordinates object. Its cshape needs to be (#source_directions). In - sperical coordinates the radii needs to be constant. The weights need - to reflect the area weights. + pyfar.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 : pyfar.FrequencyData - The random-incidence scattering coefficient. + The random-incidence scattering coefficient depending on frequency. References ---------- @@ -131,5 +141,4 @@ def random( """ random_scattering = utils.paris_formula( scattering_coefficients, incident_directions) - random_scattering.comment = 'random-incidence scattering coefficient' return random_scattering diff --git a/imkar/utils/utils.py b/imkar/utils/utils.py index 6eb8697..a460a8d 100644 --- a/imkar/utils/utils.py +++ b/imkar/utils/utils.py @@ -4,32 +4,36 @@ def paris_formula(coefficients, incident_directions): r""" - Calculate the random-incidence coefficient - according to Paris formula [2]_. + Calculate the random-incidence coefficient from free-field + data for several incident directions. + + Uses the Paris formula [2]_. .. math:: - c_{rand} = \sum c(\vartheta_S,\varphi_S) \cdot cos(\vartheta_S) \cdot w + c_{rand} = \sum c(\vartheta,\varphi) \cdot cos(\vartheta) \cdot w - with the ``coefficients``, and the - area weights ``w`` from the ``incident_directions``. - Note that the incident directions should be + with the coefficients :math:`c(\vartheta,\varphi)`, the area + weights ``w`` from the ``incident_directions.weights``, + and :math:`\vartheta` and :math:`\varphi` are the incidence + angle and azimuth angles. 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 - needs to be (..., #incident_directions) + Scattering coefficients for different incident directions. Its cshape + needs to be (..., incident_directions.csize) incident_directions : pyfar.Coordinates Defines the incidence directions of each `coefficients` in a - Coordinates object. Its cshape needs to be (#incident_directions). In - sperical coordinates the radii needs to be constant. The weights need - to reflect the area weights. + pyfar.Coordinates object. Its cshape needs to match the last dimension + of coefficients. + Points contained in ``incident_directions`` must have the same radii. + The weights need to reflect the area ``incident_directions.weights``. Returns ------- random_coefficient : pyfar.FrequencyData - The random-incidence scattering coefficient. + The random-incidence coefficient depending on frequency. References ---------- diff --git a/setup.py b/setup.py index 87153a2..705229b 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,10 @@ with open('HISTORY.rst') as history_file: history = history_file.read() -requirements = [ ] +requirements = [ + 'numpy>=1.23.0', + 'pyfar', +] test_requirements = ['pytest>=3', ] From 4d892eb272382a29d52b3b9e5a9550be0d6409d3 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Mon, 12 Jun 2023 09:59:49 +0200 Subject: [PATCH 21/32] try to add reference in doc to pyfar --- docs/conf.py | 9 +++++++++ imkar/scattering/scattering.py | 21 ++++++++++++--------- imkar/utils/utils.py | 3 ++- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 63b769a..935162e 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,6 +38,7 @@ 'sphinx.ext.napoleon', 'sphinx.ext.autosummary', 'sphinx.ext.mathjax', + 'sphinx.ext.intersphinx', 'autodocsumm'] # show tocs for classes and functions of modules using the autodocsumm @@ -99,6 +100,14 @@ # default language for highlighting in source code highlight_language = "python3" +# intersphinx mapping +intersphinx_mapping = { + 'numpy': ('https://numpy.org/doc/stable/', None), + 'scipy': ('https://docs.scipy.org/doc/scipy/', None), + 'matplotlib': ('https://matplotlib.org/stable/', None), + 'pyfar': ('https://pyfar.readthedocs.io/en/stable/', None) + } + # -- Options for HTML output ------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for diff --git a/imkar/scattering/scattering.py b/imkar/scattering/scattering.py index 7648922..c8ca2a2 100644 --- a/imkar/scattering/scattering.py +++ b/imkar/scattering/scattering.py @@ -12,10 +12,12 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): .. 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 + \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 @@ -36,7 +38,7 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): Reflected sound pressure or directivity of the reference sample. Needs to have the same cshape and frequencies as ``sample_pressure``. - microphone_weights : np.ndarray + microphone_weights : numpy.ndarray Array containing the area weights for the microphone positions. Its shape needs to match the last dimension in the cshape of ``sample_pressure`` and ``reference_pressure``. @@ -108,7 +110,8 @@ def random( Uses the Paris formula [2]_. .. math:: - s_{rand} = \sum s(\vartheta,\varphi) \cdot cos(\vartheta) \cdot w + 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`` from the ``incident_directions.weights``, @@ -123,9 +126,9 @@ def random( Scattering coefficients for different incident directions. Its cshape needs to be (..., incident_directions.csize) incident_directions : pyfar.Coordinates - Defines the incidence directions of each `scattering_coefficients` in a - pyfar.Coordinates object. Its cshape needs to match the last dimension - of scattering_coefficients. + Defines the incidence directions of each ``scattering_coefficients`` + in a :py:class:`pyfar.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``. diff --git a/imkar/utils/utils.py b/imkar/utils/utils.py index a460a8d..f3cc0e4 100644 --- a/imkar/utils/utils.py +++ b/imkar/utils/utils.py @@ -10,7 +10,8 @@ def paris_formula(coefficients, incident_directions): Uses the Paris formula [2]_. .. math:: - c_{rand} = \sum c(\vartheta,\varphi) \cdot cos(\vartheta) \cdot w + c_{rand} = \sum c(\vartheta,\varphi) \cdot cos(\vartheta) \cdot + w(\vartheta,\varphi) with the coefficients :math:`c(\vartheta,\varphi)`, the area weights ``w`` from the ``incident_directions.weights``, From f993b3976be5d22a56ceb6b132baf5926b784891 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:03:39 +0200 Subject: [PATCH 22/32] cosmetics + integrate last review comments --- imkar/scattering/scattering.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/imkar/scattering/scattering.py b/imkar/scattering/scattering.py index c8ca2a2..f1cc0b0 100644 --- a/imkar/scattering/scattering.py +++ b/imkar/scattering/scattering.py @@ -37,11 +37,11 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): reference_pressure : pyfar.FrequencyData Reflected sound pressure or directivity of the reference sample. Needs to have the same cshape and frequencies as - ``sample_pressure``. + `sample_pressure`. microphone_weights : numpy.ndarray Array containing the area weights for the microphone positions. Its shape needs to match the last dimension in the cshape of - ``sample_pressure`` and ``reference_pressure``. + `sample_pressure` and `reference_pressure`. Returns ------- @@ -114,7 +114,7 @@ def random( w(\vartheta,\varphi) with the scattering coefficients :math:`s(\vartheta,\varphi)`, the area - weights ``w`` from the ``incident_directions.weights``, + weights ``w`` from the `incident_directions.weights`, and :math:`\vartheta` and :math:`\varphi` are the incidence angle and azimuth angles. Note that the incident directions should be equally distributed to get a valid result. See @@ -126,11 +126,11 @@ def random( Scattering coefficients for different incident directions. Its cshape needs to be (..., incident_directions.csize) incident_directions : pyfar.Coordinates - Defines the incidence directions of each ``scattering_coefficients`` - in a :py:class:`pyfar.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``. + Defines the incidence directions of each `scattering_coefficients` + in a :py:class:`~pyfar.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 ------- From 02ef6db435f80a10d0bd3f0de036bf3041683cac Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:03:47 +0200 Subject: [PATCH 23/32] cosmetics + integrate last review comments --- imkar/utils/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/imkar/utils/utils.py b/imkar/utils/utils.py index f3cc0e4..7c3a202 100644 --- a/imkar/utils/utils.py +++ b/imkar/utils/utils.py @@ -14,7 +14,7 @@ def paris_formula(coefficients, incident_directions): w(\vartheta,\varphi) with the coefficients :math:`c(\vartheta,\varphi)`, the area - weights ``w`` from the ``incident_directions.weights``, + weights ``w`` from the `incident_directions.weights`, and :math:`\vartheta` and :math:`\varphi` are the incidence angle and azimuth angles. Note that the incident directions should be equally distributed to get a valid result. @@ -23,12 +23,12 @@ def paris_formula(coefficients, incident_directions): ---------- coefficients : pyfar.FrequencyData Scattering coefficients for different incident directions. Its cshape - needs to be (..., incident_directions.csize) + needs to be (..., `incident_directions.csize`) incident_directions : pyfar.Coordinates Defines the incidence directions of each `coefficients` in a pyfar.Coordinates object. Its cshape needs to match the last dimension of coefficients. - Points contained in ``incident_directions`` must have the same radii. + Points contained in `incident_directions` must have the same radii. The weights need to reflect the area ``incident_directions.weights``. Returns From 8d3030e9821904945d1e35582b334849bc8c9fc9 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:08:17 +0200 Subject: [PATCH 24/32] fix tests --- imkar/scattering/scattering.py | 3 ++- tests/test_scattering.py | 15 --------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/imkar/scattering/scattering.py b/imkar/scattering/scattering.py index f1cc0b0..4098797 100644 --- a/imkar/scattering/scattering.py +++ b/imkar/scattering/scattering.py @@ -65,7 +65,8 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): if not isinstance(reference_pressure, pf.FrequencyData): raise ValueError( "reference_pressure has to be a pyfar.FrequencyData object") - microphone_weights = np.asarray(microphone_weights) + 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 " diff --git a/tests/test_scattering.py b/tests/test_scattering.py index 7c26aed..3120d22 100644 --- a/tests/test_scattering.py +++ b/tests/test_scattering.py @@ -1,6 +1,5 @@ import pytest import numpy as np -import pyfar as pf from imkar import scattering @@ -117,17 +116,3 @@ def test_freefield_05_with_inci( assert s.freq.shape[-1] == p_sample.n_bins assert s.cshape == incident_directions.cshape assert s_rand.freq.shape[-1] == p_sample.n_bins - - -@pytest.mark.parametrize("s_value", [ - (0), (0.2), (0.5), (0.8), (1)]) -@pytest.mark.parametrize("frequencies", [ - ([100, 200]), ([100])]) -def test_random_comment( - s_value, frequencies, half_sphere): - incident_directions = half_sphere - shape = np.append(half_sphere.cshape, len(frequencies)) - s = pf.FrequencyData(np.zeros(shape)+s_value, frequencies) - s_rand = scattering.random(s, incident_directions) - np.testing.assert_allclose(s_rand.freq, s_value) - assert s_rand.comment == 'random-incidence scattering coefficient' From e005c98ba18d8629dd4072c35478fab7c6111948 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:12:35 +0200 Subject: [PATCH 25/32] fix doc --- imkar/scattering/scattering.py | 15 ++++++++------- imkar/utils/utils.py | 11 ++++++----- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/imkar/scattering/scattering.py b/imkar/scattering/scattering.py index 4098797..f0c87dd 100644 --- a/imkar/scattering/scattering.py +++ b/imkar/scattering/scattering.py @@ -31,10 +31,10 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): Parameters ---------- - sample_pressure : pyfar.FrequencyData + sample_pressure : :py:class:`~pyfar.classes.audio.FrequencyData` Reflected sound pressure or directivity of the test sample. Its cshape needs to be (..., microphone_weights.csize). - reference_pressure : pyfar.FrequencyData + 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 `sample_pressure`. @@ -45,7 +45,7 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): Returns ------- - scattering_coefficients : pyfar.FrequencyData + scattering_coefficients : :py:class:`~pyfar.classes.audio.FrequencyData` The scattering coefficient for each incident direction depending on frequency. @@ -123,19 +123,20 @@ def random( Parameters ---------- - scattering_coefficients : pyfar.FrequencyData + scattering_coefficients : :py:class:`~pyfar.classes.audio.FrequencyData` Scattering coefficients for different incident directions. Its cshape needs to be (..., incident_directions.csize) - incident_directions : pyfar.Coordinates + incident_directions : :py:class:`~pyfar.classes.coordinates.Coordinates` Defines the incidence directions of each `scattering_coefficients` - in a :py:class:`~pyfar.Coordinates` object. Its cshape needs to match + 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 : pyfar.FrequencyData + random_scattering : :py:class:`~pyfar.classes.audio.FrequencyData` The random-incidence scattering coefficient depending on frequency. References diff --git a/imkar/utils/utils.py b/imkar/utils/utils.py index 7c3a202..c82c49e 100644 --- a/imkar/utils/utils.py +++ b/imkar/utils/utils.py @@ -21,13 +21,14 @@ def paris_formula(coefficients, incident_directions): Parameters ---------- - coefficients : pyfar.FrequencyData + coefficients : :py:class:`~pyfar.classes.audio.FrequencyData` Scattering coefficients for different incident directions. Its cshape needs to be (..., `incident_directions.csize`) - incident_directions : pyfar.Coordinates - Defines the incidence directions of each `coefficients` in a - pyfar.Coordinates object. Its cshape needs to match the last dimension - of coefficients. + 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``. From ac8de6f9eba85384ec1de56f81fbed8c9a121b7f Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Mon, 7 Aug 2023 13:40:14 +0200 Subject: [PATCH 26/32] Update imkar/scattering/scattering.py Co-authored-by: Fabian Brinkmann --- imkar/scattering/scattering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imkar/scattering/scattering.py b/imkar/scattering/scattering.py index f0c87dd..e524e89 100644 --- a/imkar/scattering/scattering.py +++ b/imkar/scattering/scattering.py @@ -33,7 +33,7 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): ---------- sample_pressure : :py:class:`~pyfar.classes.audio.FrequencyData` Reflected sound pressure or directivity of the test sample. Its cshape - needs to be (..., microphone_weights.csize). + 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 From 08d0b0fef0fea333d3c74a91d939e795e077db20 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Mon, 7 Aug 2023 13:41:33 +0200 Subject: [PATCH 27/32] Update imkar/scattering/scattering.py Co-authored-by: Fabian Brinkmann --- imkar/scattering/scattering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imkar/scattering/scattering.py b/imkar/scattering/scattering.py index e524e89..5543073 100644 --- a/imkar/scattering/scattering.py +++ b/imkar/scattering/scattering.py @@ -115,7 +115,7 @@ def random( w(\vartheta,\varphi) with the scattering coefficients :math:`s(\vartheta,\varphi)`, the area - weights ``w`` from the `incident_directions.weights`, + weights ``w`` taken from the `incident_directions.weights`, and :math:`\vartheta` and :math:`\varphi` are the incidence angle and azimuth angles. Note that the incident directions should be equally distributed to get a valid result. See From ae4140d46f93a5078a874b58f628ded9c72e5836 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Mon, 7 Aug 2023 13:43:36 +0200 Subject: [PATCH 28/32] Update imkar/utils/utils.py Co-authored-by: Fabian Brinkmann --- imkar/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imkar/utils/utils.py b/imkar/utils/utils.py index c82c49e..3cc9955 100644 --- a/imkar/utils/utils.py +++ b/imkar/utils/utils.py @@ -14,7 +14,7 @@ def paris_formula(coefficients, incident_directions): w(\vartheta,\varphi) with the coefficients :math:`c(\vartheta,\varphi)`, the area - weights ``w`` from the `incident_directions.weights`, + weights ``w`` taken from the `incident_directions.weights`, and :math:`\vartheta` and :math:`\varphi` are the incidence angle and azimuth angles. Note that the incident directions should be equally distributed to get a valid result. From 4f98a5b7a4c30189aec3cb7bf8226b5b0a313768 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Mon, 7 Aug 2023 13:57:01 +0200 Subject: [PATCH 29/32] apply review @f-brinkmann --- docs/modules.rst | 1 + docs/modules/imkar.utils.rst | 7 +++++++ imkar/scattering/scattering.py | 14 ++++++++++---- imkar/utils/utils.py | 8 +++++--- 4 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 docs/modules/imkar.utils.rst diff --git a/docs/modules.rst b/docs/modules.rst index b041bb9..657f0bd 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -8,3 +8,4 @@ according to their modules. :maxdepth: 1 modules/imkar.scattering + modules/imkar.utils diff --git a/docs/modules/imkar.utils.rst b/docs/modules/imkar.utils.rst new file mode 100644 index 0000000..89d6335 --- /dev/null +++ b/docs/modules/imkar.utils.rst @@ -0,0 +1,7 @@ +imkar.utils +=========== + +.. automodule:: imkar.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/imkar/scattering/scattering.py b/imkar/scattering/scattering.py index 5543073..31d7867 100644 --- a/imkar/scattering/scattering.py +++ b/imkar/scattering/scattering.py @@ -25,7 +25,10 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): 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 incidence angle and azimuth angles. See + :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. @@ -39,7 +42,8 @@ def freefield(sample_pressure, reference_pressure, microphone_weights): reference sample. Needs to have the same cshape and frequencies as `sample_pressure`. microphone_weights : numpy.ndarray - Array containing the area weights for the microphone positions. + 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`. @@ -116,8 +120,10 @@ def random( 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 incidence - angle and azimuth angles. Note that the incident directions should be + 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. diff --git a/imkar/utils/utils.py b/imkar/utils/utils.py index 3cc9955..ac4e05a 100644 --- a/imkar/utils/utils.py +++ b/imkar/utils/utils.py @@ -15,8 +15,10 @@ def paris_formula(coefficients, incident_directions): 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 incidence - angle and azimuth angles. Note that the incident directions should be + 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 @@ -34,7 +36,7 @@ def paris_formula(coefficients, incident_directions): Returns ------- - random_coefficient : pyfar.FrequencyData + random_coefficient : :py:class:`~pyfar.classes.audio.FrequencyData` The random-incidence coefficient depending on frequency. References From 599cd70a82d592d0aff9b458e38a3740e0192053 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 20 Feb 2024 16:32:14 +0100 Subject: [PATCH 30/32] apply cookiecutter --- .circleci/config.yml | 115 ++++++++++++---------- .editorconfig | 3 + .github/ISSUE_TEMPLATE.md | 19 +--- .github/PULL_REQUEST_TEMPLATE.md | 11 --- .gitignore | 21 ++-- .readthedocs.yml | 4 +- CONTRIBUTING.rst | 164 +++++++++++++++++++------------ HISTORY.rst | 2 +- LICENSE | 2 +- MANIFEST.in | 1 - Makefile | 84 ---------------- README.rst | 14 ++- docs/conf.py | 18 +++- docs/index.rst | 7 ++ docs/make.bat | 72 +++++++------- docs/modules.rst | 4 +- docs/modules/imkar.rst | 7 ++ docs/resources/pyfar.png | Bin 0 -> 6300 bytes environment.yml | 12 +++ imkar/__init__.py | 2 + pytest.ini | 4 + requirements_dev.txt | 7 +- setup.cfg | 6 +- setup.py | 43 ++++++-- tests/test_imkar.py | 29 +++++- tox.ini | 37 ------- 26 files changed, 351 insertions(+), 337 deletions(-) delete mode 100644 Makefile create mode 100644 docs/modules/imkar.rst create mode 100644 docs/resources/pyfar.png create mode 100644 environment.yml create mode 100644 pytest.ini delete mode 100644 tox.ini diff --git a/.circleci/config.yml b/.circleci/config.yml index 63bc27a..42082da 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,32 +66,34 @@ jobs: pip-dependency-file: requirements_dev.txt - run: name: Flake8 - command: flake8 imkar - -# test_examples: -# parameters: -# version: -# description: "version tag" -# default: "latest" -# type: string -# executor: -# name: python-docker -# version: <> - -# steps: -# - checkout -# # - run: -# # name: Install System Dependencies -# # command: sudo apt-get update && sudo apt-get install -y libsndfile1 -# - python/install-packages: -# pkg-manager: pip -# # app-dir: ~/project/package-directory/ # If you're requirements.txt isn't in the root directory. -# pip-dependency-file: requirements_dev.txt -# - run: -# name: Examples -# command: | -# pip install -e . -# pytest --nbmake examples/*.ipynb + command: flake8 imkar tests + + + test_documentation_build: + parameters: + version: + description: "version tag" + default: "latest" + type: string + executor: + name: python-docker + version: <> + + steps: + - checkout + # - run: + # name: Install System Dependencies + # command: sudo apt-get update && sudo apt-get install -y libsndfile1 texlive-latex-extra dvipng + - python/install-packages: + pkg-manager: pip + # app-dir: ~/project/package-directory/ # If you're requirements.txt isn't in the root directory. + pip-dependency-file: requirements_dev.txt + - run: + name: Sphinx + command: | + pip install -e . + cd docs/ + make html SPHINXOPTS="-W" test_pypi_publish: parameters: @@ -128,9 +130,12 @@ workflows: matrix: parameters: version: - - "3.8" - - "3.9" - - "3.10" + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - flake: matrix: parameters: @@ -139,13 +144,14 @@ workflows: requires: - build_and_test - # - test_examples: - # matrix: - # parameters: - # version: - # - "3.9" - # requires: - # - build_and_test + + - test_documentation_build: + matrix: + parameters: + version: + - "3.9" + requires: + - build_and_test test_and_publish: @@ -156,9 +162,12 @@ test_and_publish: matrix: parameters: version: - - "3.8" - - "3.9" - - "3.10" + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + filters: branches: ignore: /.*/ @@ -180,19 +189,19 @@ test_and_publish: tags: only: /^v[0-9]+(\.[0-9]+)*$/ - # - test_examples: - # matrix: - # parameters: - # version: - # - "3.9" - # requires: - # - build_and_test - # filters: - # branches: - # ignore: /.*/ - # # only act on version tags - # tags: - # only: /^v[0-9]+(\.[0-9]+)*$/ + - test_documentation_build: + matrix: + parameters: + version: + - "3.9" + requires: + - build_and_test + filters: + branches: + ignore: /.*/ + # only act on version tags + tags: + only: /^v[0-9]+(\.[0-9]+)*$/ - test_pypi_publish: matrix: @@ -202,7 +211,7 @@ test_and_publish: requires: - build_and_test - flake - # - test_examples + - test_documentation_build filters: branches: ignore: /.*/ diff --git a/.editorconfig b/.editorconfig index d4a2c44..0908478 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,6 +10,9 @@ insert_final_newline = true charset = utf-8 end_of_line = lf +[*.yml] +indent_size = 2 + [*.bat] indent_style = tab end_of_line = crlf diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 7acd92e..b8e513f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,27 +1,18 @@ +## General + * imkar version: * Python version: * Operating System: +* Did you install pyfar via pip: -### Description +## Description Describe what you were trying to get done. Tell us what happened, what went wrong, and what you expected to happen. -### What I Did +## What I Did ``` Paste the command(s) you ran and the output. If there was a crash, please include the traceback here. ``` - -## Labels - -Label your issue to make it easier for us to assign and track: - -Use one of these labels: -- **hot:** For bugs on the master branch -- **bug:** For bugs not on the master branch -- **enhancement:** For suggesting enhancements of current functionality -- **feature:** For requesting new features -- **documentation:** Everything related to docstrings and comments -- **question:** General questions, e.g., regarding the general structure or future directions diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3379f97..dd5be6f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,14 +7,3 @@ Closes # - - - - -### Labels - -Label your issue to make it easier for us to assign and track: - -Use one of these labels: -- **hot:** For bugs on the master branch -- **bug:** For bugs not on the master branch -- **enhancement:** For suggesting enhancements of current functionality -- **feature:** For requesting new features -- **documentation:** Everything related to docstrings and comments diff --git a/.gitignore b/.gitignore index 7fc851d..ede6d71 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ __pycache__/ # Distribution / packaging .Python -env/ build/ develop-eggs/ dist/ @@ -24,6 +23,7 @@ wheels/ *.egg-info/ .installed.cfg *.egg +MANIFEST # PyInstaller # Usually these files are written by a python script from a template @@ -37,7 +37,6 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ -.tox/ .coverage .coverage.* .cache @@ -54,6 +53,7 @@ coverage.xml # Django stuff: *.log local_settings.py +db.sqlite3 # Flask stuff: instance/ @@ -64,12 +64,14 @@ instance/ # Sphinx documentation docs/_build/ +docs/_autosummary # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints +*/.ipynb_checkpoints/* # pyenv .python-version @@ -80,13 +82,14 @@ celerybeat-schedule # SageMath parsed files *.sage.py -# dotenv +# Environments .env - -# virtualenv .venv +env/ venv/ ENV/ +env.bak/ +venv.bak/ # Spyder project settings .spyderproject @@ -101,9 +104,13 @@ ENV/ # mypy .mypy_cache/ -# IDE settings +# vs code .vscode/ .idea/ -# OS stuff +# macOS .DS_Store + +# workaround for failing test discovery in vscode +tests/*/__init__.py +tests/private/ diff --git a/.readthedocs.yml b/.readthedocs.yml index 9a269eb..099ecc1 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,8 +9,8 @@ build: os: ubuntu-22.04 tools: python: "3.10" - apt_packages: - - libsndfile1 + # apt_packages: + # - libsndfile1 # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 8c30f2a..76845c5 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -7,66 +7,59 @@ Contributing Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. -You can contribute in many ways: - Types of Contributions ---------------------- -Report Bugs -~~~~~~~~~~~ - -Report bugs at https://github.com/pyfar/imkar/issues. +Report Bugs or Suggest Features +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Fix Bugs -~~~~~~~~ +The best place for this is https://github.com/pyfar/imkar/issues. -Look through the GitHub issues for bugs. Anything tagged with "bug" and "help -wanted" is open to whoever wants to implement it. +Fix Bugs or Implement Features +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Implement Features -~~~~~~~~~~~~~~~~~~ - -Look through the GitHub issues for features. Anything tagged with "enhancement" -and "help wanted" is open to whoever wants to implement it. +Look through https://github.com/pyfar/imkar/issues for bugs or feature request +and contact us or comment if you are interested in implementing. Write Documentation ~~~~~~~~~~~~~~~~~~~ -https://github.com/pyfar/imkar could always use more documentation, whether as part of the +imkar could always use more documentation, whether as part of the official imkar docs, in docstrings, or even on the web in blog posts, articles, and such. Get Started! ------------ -Ready to contribute? Here's how to set up `imkar` for local development. +Ready to contribute? Here's how to set up `imkar` for local development using the command-line interface. Note that several alternative user interfaces exist, e.g., the Git GUI, `GitHub Desktop `_, extensions in `Visual Studio Code `_ ... -1. Fork the `imkar` repo on GitHub. -2. Clone your fork locally:: +1. `Fork `_ the `imkar` repo on GitHub. +2. Clone your fork locally and cd into the imkar directory:: - $ git clone https://github.com/pyfar/imkar.git + $ git clone https://github.com/YOUR_USERNAME/imkar.git + $ cd imkar -3. Install your local copy into a virtual environment. Assuming you have Anaconda or Miniconda installed, this is how you set up your fork for local development:: +3. Install your local copy into a virtualenv. Assuming you have Anaconda or Miniconda installed, this is how you set up your fork for local development:: - $ conda create --name imkar python pip + $ conda create --name imkar python $ conda activate imkar - $ cd imkar - $ pip install -r requirements_dev.txt + $ conda install pip $ pip install -e . + $ pip install -r requirements_dev.txt -4. Create a branch for local development:: +4. Create a branch for local development. Indicate the intention of your branch in its respective name (i.e. `feature/branch-name` or `bugfix/branch-name`):: $ git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 5. When you're done making changes, check that your changes pass flake8 and the - tests, including testing other Python versions with tox:: + tests:: $ flake8 imkar tests $ pytest - To get flake8 and tox, just pip install them into your virtualenv. + flake8 test must pass without any warnings for `./imkar` and `./tests` using the default or a stricter configuration. Flake8 ignores `E123/E133, E226` and `E241/E242` by default. If necessary adjust the your flake8 and linting configuration in your IDE accordingly. 6. Commit your changes and push your branch to GitHub:: @@ -74,7 +67,7 @@ Ready to contribute? Here's how to set up `imkar` for local development. $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature -7. Submit a pull request through the GitHub website. +7. Submit a pull request on the develop branch through the GitHub website. Pull Request Guidelines ----------------------- @@ -82,37 +75,76 @@ Pull Request Guidelines Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. -2. If the pull request adds functionality, the docs should be updated. Put - your new functionality into a function with a docstring. -3. Check https://app.circleci.com/pipelines/github/pyfar/imkar - and make sure that the tests pass for all supported Python versions. +2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring. +3. If checks do not pass, have a look at https://app.circleci.com/pipelines/github/pyfar/imkar for more information. + +Function and Class Guidelines +----------------------------- + +Functions and classes should + +* have a single clear purpose and a functionality limited to that purpose. Conditional parameters are fine in some cases but are an indicator that a function or class does not have a clear purpose. Conditional parameters are + + - parameters that are obsolete if another parameter is provided + - parameters that are necessary only if another parameter is provided + - parameters that must have a specific value depending on other parameters + +* be split into multiple functions or classes if the functionality not well limited. +* contain documentation for all input and output parameters. +* contain examples in the documentation if they are non-trivial to use. +* contain comments in the code that explain decisions and parts that are not trivial to read from the code. As a rule of thumb, too much comments are better than to little comments. +* use clear names for all variables + +It is also a good idea to follow `the Zen of Python `_ + +Errors should be raised if + +* Audio objects do not have the correct type (e.g. a TimeData instance is passed but a Signal instance is required) +* String input that specifies a function option has an invalid value (e.g. 'linea' was passed but 'linear' was required) +* Invalid parameter combinations are used + +Warnings should be raised if + +* Results might be wrong or unexpected +* Possibly bad parameter combinations are used Testing Guidelines ----------------------- -imkar uses test-driven development based on `three steps `_ and `continuous integration `_ to test and monitor the code. -In the following, you'll find a guideline. +Pyfar uses test-driven development based on `three steps `_ and `continuous integration `_ to test and monitor the code. +In the following, you'll find a guideline. Note: these instructions are not generally applicable outside of pyfar. - The main tool used for testing is `pytest `_. - All tests are located in the *tests/* folder. - Make sure that all important parts of imkar are covered by the tests. This can be checked using *coverage* (see below). - In case of imkar, mainly **state verification** is applied in the tests. This means that the outcome of a function is compared to a desired value (``assert ...``). For more information, it is refered to `Martin Fowler's article `_. +Required Tests +~~~~~~~~~~~~~~ + +The testing should include + +- Test all errors and warnings (see also function and class guidelines above) +- Test all parameters +- Test specific parameter combinations if required +- Test with single and multi-dimensional input data such Signal objects and array likes +- Test with audio objects with complex time data and NaN values (if applicable) + Tips ~~~~~~~~~~~ Pytest provides several, sophisticated functionalities which could reduce the effort of implementing tests. - Similar tests executing the same code with different variables can be `parametrized `_. An example is ``test___eq___differInPoints`` in *test_coordinates.py*. -- Run a single test with:: +- Run a single test with $ pytest tests/test_plot.py::test_line_plots -- Exclude tests (for example the time consuming test of plot) with:: +- Exclude tests (for example the time consuming test of plot) with - $ pytest -k 'not plot' + $ pytest -k 'not plot and not interaction' -- Create an html report on the test `coverage `_ with:: +- Create an html report on the test `coverage `_ with $ pytest --cov=. --cov-report=html @@ -120,9 +152,7 @@ Pytest provides several, sophisticated functionalities which could reduce the ef Fixtures ~~~~~~~~ -This section is not specific to imkar, but oftentimes refers to features and examples implemented in the pyfar package which is one of the main dependencies of `imkar `_. - -"Software test fixtures initialize test functions. They provide a fixed baseline so that tests execute reliably and produce consistent, repeatable, results. Initialization may setup services, state, or other operating environments. These are accessed by test functions through arguments; for each fixture used by a test function there is typically a parameter (named after the fixture) in the test function’s definition." (from https://docs.pytest.org/en/stable/fixture.html) +"Software test fixtures initialize test functions. They provide a fixed baseline so that tests execute reliably and produce consistent, repeatable, results. Initialization may setup services, state, or other operating environments. These are accessed by test functions through parameters; for each fixture used by a test function there is typically a parameter (named after the fixture) in the test function’s definition." (from https://docs.pytest.org/en/stable/fixture.html) - All fixtures are implemented in *conftest.py*, which makes them automatically available to all tests. This prevents from implementing redundant, unreliable code in several test files. - Typical fixtures are imkar objects with varying properties, stubs as well as functions need for initiliazing tests. @@ -133,32 +163,31 @@ Have a look at already implemented fixtures in *confest.py*. **Dummies** If the objects used in the tests have arbitrary properties, tests are usually better to read, when these objects are initialized within the tests. If the initialization requires several operations or the object has non-arbitrary properties, this is a hint to use a fixture. -Good examples illustrating these two cases are the initializations in pyfar's *test_signal.py* vs. the sine and impulse signal fixtures in pyfar's *conftest.py*. +Good examples illustrating these two cases are the initializations in *test_signal.py* vs. the sine and impulse signal fixtures in *conftest.py*. **Stubs** -Stubs mimic actual objects, but have minimum functionality and **fixed, well defined properties**. They are **only used in cases, when a dependence on the actual class is prohibited**. -This is the case, when functionalities of the class itself or methods it depends on are tested. Examples are the tests of the pyfar Signal class and its methods in *test_signal.py* and *test_fft.py*. +Stubs mimic actual objects, but have minimum functionality and **fixed, well defined properties**. They are **only used in cases, when a dependence on the actual imkar class is prohibited**. This is the case, when functionalities of the class itself or methods it depends on are tested. Examples are the tests of the Signal class and its methods in *test_signal.py* and *test_fft.py*. -It requires a little more effort to implement stubs of classes. Therefore, stub utilities are provided in and imported in *confest.py*, where the actual stubs are implemented. +It requires a little more effort to implement stubs of the imkar classes. Therefore, stub utilities are provided in *imkar/testing/stub_utils.py* and imported in *confest.py*, where the actual stubs are implemented. - Note: the stub utilities are not meant to be imported to test files directly or used for other purposes than testing. They solely provide functionality to create fixtures. -- The utilities simplify and harmonize testing within package and improve the readability and reliability. -- The implementation as the private submodule ``pyfar.testing.stub_utils`` further allows the use of similar stubs in related packages with pyfar dependency (e.g. other packages from the pyfar family). +- The utilities simplify and harmonize testing within the imkar package and improve the readability and reliability. +- The implementation as the private submodule ``imkar.testing.stub_utils`` further allows the use of similar stubs in related packages with imkar dependency (e.g. other packages from the pyfar} family). **Mocks** Mocks are similar to stubs but used for **behavioral verification**. For example, a mock can replace a function or an object to check if it is called with correct parameters. A main motivation for using mocks is to avoid complex or time-consuming external dependencies, for example database queries. -- A typical use case of mocks in the pyfar context is hardware communication, for example reading and writing of large files or audio in- and output. These use cases are rare compared to tests performing state verification. +- A typical use case of mocks in the imkar context is hardware communication, for example reading and writing of large files or audio in- and output. These use cases are rare compared to tests performing state verification. - In contrast to some other guidelines on mocks, external dependencies do **not** need to be mocked in general. Failing tests due to changes in external packages are meaningful hints to modify the code. -- Examples of internal mocking can be found in pyfar's *test_io.py*, indicated by the pytest ``@patch`` calls. +- Examples of internal mocking can be found in *test_io.py*, indicated by the pytest ``@patch`` calls. Writing the Documentation ------------------------- -imkar follows the `numpy style guide `_ for the docstring. A docstring has to consist at least of +Pyfar follows the `numpy style guide `_ for the docstring. A docstring has to consist at least of - A short and/or extended summary, - the Parameters section, and @@ -178,45 +207,50 @@ Here are a few tips to make things run smoothly - Use ``[#]_`` and ``.. [#]`` to get automatically numbered footnotes. - Do not use footnotes in the short summary. Only use footnotes in the extended summary if there is a short summary. Otherwise, it messes with the auto-footnotes. - If a method or class takes or returns pyfar objects for example write ``parameter_name : Signal``. This will create a link to the ``pyfar.Signal`` class. -- Plots can be included in by using the prefix ``.. plot::`` followed by an empty line and an indented block containing the code for the plot. +- Plots can be included in by using the prefix ``.. plot::`` followed by an empty line and an indented block containing the code for the plot. See `pyfar.plot.line.time.py` for examples. See the `Sphinx homepage `_ for more information. Building the Documentation -------------------------- -You can build the documentation of your branch using Sphinx by executing the make script inside the docs folder:: +You can build the documentation of your branch using Sphinx by executing the make script inside the docs folder. + +.. code-block:: console $ cd docs/ $ make html -After Sphinx finishes you can open the generated html using any browser:: +After Sphinx finishes you can open the generated html using any browser + +.. code-block:: console $ docs/_build/index.html Note that some warnings are only shown the first time you build the -documentation. To show the warnings again use:: +documentation. To show the warnings again use + +.. code-block:: console $ make clean before building the documentation. - Deploying ---------- +~~~~~~~~~ A reminder for the maintainers on how to deploy. -Make sure all your changes are committed (including an entry in HISTORY.rst). -Then run:: - $ bump2version patch # possible: major / minor / patch - $ git push - $ git push --tags +- Commit all changes to develop +- Update HISTORY.rst in develop +- Merge develop into main + +Switch to main and run:: -CircleCI will then deploy to PyPI if tests pass. +$ bumpversion patch # possible: major / minor / patch +$ git push --follow-tags -To manually build the package and upload to pypi run:: +The testing platform will then deploy to PyPI if tests pass. - $ python setup.py sdist bdist_wheel - $ twine upload dist/* +- merge main back into develop diff --git a/HISTORY.rst b/HISTORY.rst index 009592a..917a0b9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,7 +2,7 @@ History ======= -0.1.0 (2022-09-19) +0.1.0 (2024-02-20) ------------------ * First release on PyPI. diff --git a/LICENSE b/LICENSE index c47548d..e31563b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022, The pyfar developers +Copyright (c) 2024, The pyfar developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index 965b2dd..292d6dd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ -include AUTHORS.rst include CONTRIBUTING.rst include HISTORY.rst include LICENSE diff --git a/Makefile b/Makefile deleted file mode 100644 index 235fdff..0000000 --- a/Makefile +++ /dev/null @@ -1,84 +0,0 @@ -.PHONY: clean clean-build clean-pyc clean-test coverage dist docs help install lint lint/flake8 -.DEFAULT_GOAL := help - -define BROWSER_PYSCRIPT -import os, webbrowser, sys - -from urllib.request import pathname2url - -webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) -endef -export BROWSER_PYSCRIPT - -define PRINT_HELP_PYSCRIPT -import re, sys - -for line in sys.stdin: - match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) - if match: - target, help = match.groups() - print("%-20s %s" % (target, help)) -endef -export PRINT_HELP_PYSCRIPT - -BROWSER := python -c "$$BROWSER_PYSCRIPT" - -help: - @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) - -clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts - -clean-build: ## remove build artifacts - rm -fr build/ - rm -fr dist/ - rm -fr .eggs/ - find . -name '*.egg-info' -exec rm -fr {} + - find . -name '*.egg' -exec rm -f {} + - -clean-pyc: ## remove Python file artifacts - find . -name '*.pyc' -exec rm -f {} + - find . -name '*.pyo' -exec rm -f {} + - find . -name '*~' -exec rm -f {} + - find . -name '__pycache__' -exec rm -fr {} + - -clean-test: ## remove test and coverage artifacts - rm -fr .tox/ - rm -f .coverage - rm -fr htmlcov/ - rm -fr .pytest_cache - -lint/flake8: ## check style with flake8 - flake8 imkar tests - -lint: lint/flake8 ## check style - -test: ## run tests quickly with the default Python - pytest - -test-all: ## run tests on every Python version with tox - tox - -coverage: ## check code coverage quickly with the default Python - coverage run --source imkar -m pytest - coverage report -m - coverage html - $(BROWSER) htmlcov/index.html - -docs: ## generate Sphinx HTML documentation, including API docs - $(MAKE) -C docs clean - $(MAKE) -C docs html - $(BROWSER) docs/_build/html/index.html - -servedocs: docs ## compile the docs watching for changes - watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . - -release: dist ## package and upload a release - twine upload dist/* - -dist: clean ## builds source and wheel package - python setup.py sdist - python setup.py bdist_wheel - ls -l dist - -install: clean ## install the package to the active Python's site-packages - python setup.py install diff --git a/README.rst b/README.rst index 7da39b8..f2c7e19 100644 --- a/README.rst +++ b/README.rst @@ -6,21 +6,27 @@ imkar .. image:: https://img.shields.io/pypi/v/imkar.svg :target: https://pypi.python.org/pypi/imkar -.. image:: https://img.shields.io/cirrus/github/pyfar/imkar.svg - :target: https://app.circleci.com/pipelines/github/pyfar/imkar +.. image:: https://img.shields.io/travis/pyfar/imkar.svg + :target: https://travis-ci.com/pyfar/imkar .. image:: https://readthedocs.org/projects/imkar/badge/?version=latest :target: https://imkar.readthedocs.io/en/latest/?version=latest :alt: Documentation Status + + A python package for material modeling and quantification in acoustics. + +* Free software: MIT license + + Getting Started =============== Check out `read the docs`_ for the complete documentation. Packages -related to imkar are listed at `pyfar.org`_. +related to pyfar are listed at `pyfar.org`_. Installation ============ @@ -39,6 +45,6 @@ Contributing Refer to the `contribution guidelines`_ for more information. -.. _contribution guidelines: https://github.com/pyfar/imkar/blob/main/CONTRIBUTING.rst +.. _contribution guidelines: https://github.com/pyfar/imkar/blob/develop/CONTRIBUTING.rst .. _pyfar.org: https://pyfar.org .. _read the docs: https://imkar.readthedocs.io/en/latest diff --git a/docs/conf.py b/docs/conf.py index 63b769a..012fbc1 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ import sys sys.path.insert(0, os.path.abspath('..')) -import imkar # noqa +import imkar # -- General configuration --------------------------------------------- @@ -37,8 +37,11 @@ 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 'sphinx.ext.autosummary', + 'matplotlib.sphinxext.plot_directive', 'sphinx.ext.mathjax', - 'autodocsumm'] + 'sphinx.ext.intersphinx', + 'autodocsumm', + ] # show tocs for classes and functions of modules using the autodocsumm # package @@ -62,7 +65,7 @@ # General information about the project. project = 'imkar' -copyright = "2022, The pyfar developers" +copyright = "2024, The pyfar developers" author = "The pyfar developers" # The version info for the project you're documenting, acts as replacement @@ -99,6 +102,14 @@ # default language for highlighting in source code highlight_language = "python3" +# intersphinx mapping +intersphinx_mapping = { +'numpy': ('https://numpy.org/doc/stable/', None), +'scipy': ('https://docs.scipy.org/doc/scipy/', None), +'matplotlib': ('https://matplotlib.org/stable/', None), +'pyfar': ('https://pyfar.readthedocs.io/en/stable/', None), + } + # -- Options for HTML output ------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for @@ -178,3 +189,4 @@ 'One line description of project.', 'Miscellaneous'), ] + diff --git a/docs/index.rst b/docs/index.rst index 3326b76..fb0146a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,3 +1,10 @@ +.. |pyfar_logo| image:: resources/pyfar.png + :width: 150 + :alt: Alternative text + +|pyfar_logo| + + Getting Started =============== diff --git a/docs/make.bat b/docs/make.bat index 29e71ed..e2891c9 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,36 +1,36 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=python -msphinx -) -set SOURCEDIR=. -set BUILDDIR=_build -set SPHINXPROJ=imkar - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The Sphinx module was not found. Make sure you have Sphinx installed, - echo.then set the SPHINXBUILD environment variable to point to the full - echo.path of the 'sphinx-build' executable. Alternatively you may add the - echo.Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=python -msphinx +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=imkar + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The Sphinx module was not found. Make sure you have Sphinx installed, + echo.then set the SPHINXBUILD environment variable to point to the full + echo.path of the 'sphinx-build' executable. Alternatively you may add the + echo.Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/modules.rst b/docs/modules.rst index 52a15ec..f04a5a0 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1,10 +1,10 @@ Modules ======= -The following gives detailed information about all imkar functions sorted +The following gives detailed information about all pyfar functions sorted according to their modules. .. toctree:: :maxdepth: 1 - + modules/imkar diff --git a/docs/modules/imkar.rst b/docs/modules/imkar.rst new file mode 100644 index 0000000..0ea5287 --- /dev/null +++ b/docs/modules/imkar.rst @@ -0,0 +1,7 @@ +imkar +===== + +.. automodule:: imkar + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/resources/pyfar.png b/docs/resources/pyfar.png new file mode 100644 index 0000000000000000000000000000000000000000..e21a816e8354f3d4923fa27b0a562b8dab50fcc9 GIT binary patch literal 6300 zcmb_>`9GB3`~N*->^mdID@-I~60($J8KTuPY9z8%WGRG13`Qz7L@Fuk*at<)QkFr5 zDX%b=v{}l&H5ltm%;)I+dOW`W!1o6a_kHf`T<4tYoa^~~UT5y&ZH|Zv$qPXcBzo+q z#YqT);~@wpDZmF>D#e~kf`5WnjyeWIkfzGkA1tiE^cn=o*&MSlBZOtl4cG?#bL<-b zuy5SsM0CS@ay_hS`FMtamZ*Sh1Ou*#9-#E8$+6)iHsUs&NALO#Nn7n-LT3Y+cLE8! z^$+QQ|zb`Kq!T0f`vEJ|CchYfuHm)wW95eMaR@~?&#GBwMWUWY>><#VYufz;TM2|j@mMgOBh_a7}#qA``r`!&J^>6LjnG0P z!wd(?Qkg_^l9rD$|C-@X!L?mrVjn$Teq`V_)8`SyB#3vu^M^9unkGzWIJm#8k|)Xn zbe1-LJKU`XUy}sZpU%y%l$gLwcj90~88qR|#idPl=at@I-4`Fu%dCgGrmapb6x0n3 zFhQ*1u!GhuyW;zbSgZlolEi_ZCW9MG**z1fF8#)(*NYrx4L6cklG^3X?G4qr2j*2b z=YA1K;(f&nUtQ;3HxvwK*KkDe4_mN7XOCQ}bKOKP4DO0gVZH4i$bYe{?(8DuAg=59 zEUS=mH4WZv=Na&G+EoVgHY2~SbD&Ln5bRg_W?&%gE3)>xR%|v`Z;nUtoME~WKmUUU zg*-6XhUYJ|j!Z46J8wQPdAEP6ZXv1dTEibF%hK!WdJRWY7QVrIOeHBFgYLiW?NqmF zn5z9f`-^z$U!@Cfs_7l$`(u=%V7{igsU@qZ{9vupcQl1a_V3>&e79znHX0-gH@_%F zdWY**D;-axxA(;rgEeo27^XKKleu-$;&jeZ-|UgmsQSTRYUsbH^oPCE^Zg&rFOS;< z*q&mq8iOogl9%O5|AiA2qG;0jV!~g3(S;ax=gz%6o$U67Bi4n*CpdC9HX?=`J|Dji z5;g8&ONpuNACub!wE>cp>2B^0T6V|yA@2F4-?1l?!){Jp-qf?#&YOM3oze?Cn}h5; zdEan`r}a$I@#)~{^x16L7YA|>RQ8fuIybXpJUyqxZVLORv?&#z9F{?c!XpA{3@$>v zzvg2739}1?Zt>jqy$+ebr6|*VMVxkmpPadejPFvT^9@TrYNS$V-(ja-{)EQ-@oBrG zG4rdBu^-`t0UJIMyBvp)GB2zg3kWICJc(vY)#g*5F9_JF8(G#|LmnZO3^hyaz>03v zixLG-92A#UFj`$#Ro{7NYzKBj;#=jHb`0*`JM<3hYoW1Gwsvwe>tg4K@J)|3Z=k%Y&fr~u`s)9={0kG zGvV#tKd(}&G{y?1Pxb82*hXnppE&92A#=Hhjs_9;8#>`Y~k2J zdePE!hc_R|r1jzOMB@~5VA~&gRrTk4SL_Q--&@z(x*=q!oVy~b97VS)YxJ`)1%9`E z1FOrC0>n-E`r~cIGI3L!x)VbO4n?PSaG3S< zU>A1rB13v8oZ@%G%z z#dedLDdFzWsfV(j++O@TVYDYNvrzjf=inc7=D|S`ngdO9VsXIFaNovA zMLr&-KuLRKv%>6wMW>XJBlMeOgx_omwPVA|YD&oh+ZZ0yxthqUyQ&`h+B<*G@{KBXKkFoBMEc;gV!~!oq zlcxN}MJu~dI$X1$T5HtzireM>b_ZUq3qtq~CaaFAP^hZP%<4vmB*~7xcQZiqw=a0q9 zP<=Ixq$zxl%n6CA51bp%YmAJ^8q+8(r7wp~f)ehiFWjDHj3|^8hYRE1MMyfonbL8+ z@y~j5XJ5O0I^DgPjTpLr*u{biX->Na`@pZ~&pb$PZQO|y)o^Lv7RAi7GPYo-Ik$&} zddRs^xPmmTddBsNGbw~yWLH{g$*(=Ql%E=hWUy_1F1c-}Gp(GAO*OgGiApF{%@NCD zU-t^go}{O4#1`M%uXJwom;>*0a58Kf`wkI7b#n42`|k=U4l&~;^P}4H=OzRBEQbn= zUeUj+pP|Ka?w0jdFJ(IPCGCTiNcf20@-^DsP=j&U-nPx6$qn~)vEOYeCHJH#A9WrV zcLwlepCz#|Iuif5L8}voC;r!-^^iV&E09mglJ6LZZQ~@=XC-}IUerz$R-$oXy-#Vt zwCAdfF92}R%kxWFDx8pqz^rKA(|s-Y!Sdi$ZY47E#eupbX0%S!sgpiB2g zlVcdbiGl#$BHnqMq3`94m`yd3`PS?2QlDHQ{mf6C)$0;#QgCFG7y@Hn;F(j})bCv& zj$MqDciZB1@^utZNBVJY4@556YM}968MDxw9p;3RWFj6r&d=75E(Nb{vF}o%hAS zN|d~=82v(1xOaa6Ei{7sZF}m9VbCM*yO5x)fx#wgO?jh1 z&`Qr*9O>vG=cWA_jJ?2%#sBH@cp1JIaFuIo<5ic{lVNZf%;I#ZLgx+Fh4rIJo*pEr zthF*gYuII3A**>_8BCCn@i|*BmYryy73+C|7P{`k`4B_kAnUEEuX}xQ1s}C0rqf!r`?F?T z=%=6}*p51p5fNWzEO{`hJ@j6>#_4jNxNqjt2+VD%>r&+n;@BWgK!TR{*;>K*($A|r zMvtKuymD%?4OYoZ0!1NYhxtv3DcX|GV;E|S*iyU?(QFjqy4kV7@kx&)hO#WZ96HsQ9$xT>w?KH_uWr5 zE069<(tT|(L}2k|w#r(#qCD5j&OL0IZXq?;?sw_WT+VKu3t#uYeB@lk89}_L6h+QT z$-nT`y5DdryS}1J!bM-2@@=2Ra&fs{!(dseNkl>-)Y7HNQxw-o-+vLri|k>)ceRwkg0yoO zs@l^()Lb;5|Lwyh_=2@=N!;#*Z`aA%R8uzoR2h0pEo}$R?gsr~#2yw!xrT-PG?43Z z;BiH?OxD&Z6;w`573*F+Go{=ndxoaM4jArv0{6ZRwG4MZEBxLxhpFAlw7iFA%p!?3{oLYa7ynODObQJquF}6`jKG=jw@ZoLggKUBUSG{d;I7d zzYJ=&?4A~yCwp(&PMqBXx?4p-qVAnqad)rv#O?n^<=VrMEnq|R`uJ1M5n~`;2@6uL zUI0%`QKbo$KKDkS2B8xnOCRObg)EtU9q0;p0UIH46xcA-HkC{8s8-q6Izs6Ckfj|! z9}EBuLprFL5Kipf^aq2K4s$RP=rUzi7+@FtoUk9v%6us zmt*UR;G(~K%(K<%jcP@WI<>}&`C`G~g7{vv&h}B?ok=3TTa({~vx}ueDVP32uB0S| zYIZf+)_aRFOhIJbox$W&TT#yZ{$5v=2;Mj#!O*5KvQw`Sh!Uf#uhn5%H5UR zZm(uGBBIRmXj-}nhrswsDaftJU+Z4Gkr!z)A-iM>69G$=2WyzEb5p-+^tNUFFt3n1 z8jh?gE*Ad?;+rf*p<0ht^fl93QTYqJohu8@Lx4Knad4#lu~RjdzML2B%_(tI|K&5>IX)@j=;1k2R)sc;x?c^9X8rVsZLXR^)|@n6ZN-K0$oK zv8A!i>{|tYmBR9WHs1MTWf{PEsck02i`RsaU)~i_aN{eaZx+qy4GEFppr5tJP**Cj z*^Ms&3zeqGrE5gu!MUomCY>jA_x`2_@@1FUFh+z46i8?nZtvmbzVwNy?8i(O5iULf9EucdT}MP^Lr)_Qrtda*jVjZ4G9r6w=}j)ZLw zoH^*8IL3j`sy}opxqXqEi1h6sKaMwhyp+oIP6VP86sO8-t(7kCU?q<|_x7pzu6$(V>BD=Q03Cr_2`_8r z%`;U9=?z;8vg^mFeU8tRnc{*il9bz_k8{|)RpB?vJTscN(usVOsWaPmrR203*wPVXIM#1md5F{k0@gpHV+`F5m0m5tP|s@r+;EX zmE_!Tujj&_a*n*uR*UbuSaC+dur?tDN--0%2>>@-Gq{>dc3XA^`$ivdUaizK8!~`g z;8ky(yqhPlGLSZ=CrUR$T{%TCFu_*A3k;8HrYFcgCkEZe{%!;+tj6ab&6=44=FSpX z1&XVc7whxI*zIE5pU_52o+L%$ncxB^zM%OWMt! zoo2~T&0BIe2$B4o?G2Nx<7L(wEP+`S&*2HPRR@hc4Odv|a(^t5z%I1u<)*I#DO%lJ65 zYDtkJa(gS^6c~jWz16h6TwkaZrXhM_Qi1 zl;U;JOoH^q-u8$A$T-9=*B**IbmvAKxB^)%onJx+@P`ka9TCQS%9IEh8E)DTz%Nfn z@7X-o*%K>j&s#w}l6>+`xZ!Y;FqI717ezgoe_1-G=t)J7Jh6L5dr1V5bePyk^+gi1 zA^X%)LWs`9R5GB09X1kJJTjop)&Hdav_pd(kgnjO=0x)pq0gs0DDLT{=2WMAU&4^` zm5dvY$jBcH?>jJ`Lu!NB|ELXUN6JM$U05b$Q#&%xHr9g^#nL0air#LJR%rzi$l5T6 zyKqHiIBdfJTp47sRCzHaj~}mWu5B;U4oOo~OVoINR}Am4>YZk6lhhV=ni0sk$}Qd` zr;7V<>rZ*gTSd1pSRz{f#O6&4tqYumAo0+5gw?(H%)xaNCZ|ZO$MjM;VW5yX6UkoM zh*naKR2(WA`!ODZg786H75VctqN2?*l#{I;&@Z0)K6 zcbUVH&;O&3k)1e3Ef8ySS+}A8J=X~WAWL`zS-t}LeCeLevgzNqb^y2lK@)(?X$N9v z9?-0HTE}=?^N274Lz0xHc-wm{7x#dg(I0pxZ+E+L0q7H;YAUgRb(eM!9Qo;kHVufp zCOT-vRC1?&5%b`@TOan2XQRVsq=3', ] +setup_requirements = [ + 'pytest-runner', +] + +test_requirements = [ + 'pytest', + 'bump2version', + 'wheel', + 'watchdog', + 'flake8', + 'coverage', + 'Sphinx', + 'twine' +] setup( author="The pyfar developers", author_email='info@pyfar.org', - python_requires='>=3.8', classifiers=[ 'Development Status :: 2 - Pre-Alpha', - 'Intended Audience :: Scientists', + 'Intended Audience :: Science/Research', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + ], description="A python package for material modeling and quantification in acoustics.", install_requires=requirements, license="MIT license", - long_description=readme + '\n\n' + history, + long_description=readme, include_package_data=True, keywords='imkar', name='imkar', - packages=find_packages(include=['imkar', 'imkar.*']), + packages=find_packages(), + setup_requires=setup_requirements, test_suite='tests', tests_require=test_requirements, - url='https://github.com/pyfar/imkar', + url="https://pyfar.org/", + download_url="https://pypi.org/project/imkar/", + project_urls={ + "Bug Tracker": "https://github.com/pyfar/imkar/issues", + "Documentation": "https://imkar.readthedocs.io/", + "Source Code": "https://github.com/pyfar/imkar", + }, version='0.1.0', zip_safe=False, + python_requires='>=3.8', ) diff --git a/tests/test_imkar.py b/tests/test_imkar.py index e6d07f7..f801097 100644 --- a/tests/test_imkar.py +++ b/tests/test_imkar.py @@ -1,5 +1,24 @@ -def test_import_imkar(): - try: - import imkar # noqa - except ImportError: - assert False +#!/usr/bin/env python + +"""Tests for `imkar` package.""" + +import pytest + + +from imkar import imkar + + +@pytest.fixture +def response(): + """Sample pytest fixture. + + See more at: http://doc.pytest.org/en/latest/fixture.html + """ + # import requests + # return requests.get('https://github.com/mberz/cookiecutter-pypackage') + + +def test_content(response): + """Sample pytest test function with the pytest fixture as an argument.""" + # from bs4 import BeautifulSoup + # assert 'GitHub' in BeautifulSoup(response.content).title.string diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 480bdc2..0000000 --- a/tox.ini +++ /dev/null @@ -1,37 +0,0 @@ -[tox] -envlist = py36, py37, py38, flake8 - -[travis] -python = - 3.8: py38 - 3.7: py37 - 3.6: py36 - -[testenv:flake8] -basepython = python -deps = flake8 -commands = flake8 imkar tests - -# Release tooling -[testenv:build] -basepython = python3 -skip_install = true -deps = - wheel - setuptools -commands = - python setup.py -q sdist bdist_wheel - - -[testenv] -setenv = - PYTHONPATH = {toxinidir} -deps = - -r{toxinidir}/requirements_dev.txt -; If you want to make tox run the tests with the same versions, create a -; requirements.txt with the pinned versions and uncomment the following line: -; -r{toxinidir}/requirements.txt -commands = - pip install -U pip - pytest --basetemp={envtmpdir} - From 723c48c958adebb3842c1e1ec4f048889a1a3375 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 20 Feb 2024 16:38:40 +0100 Subject: [PATCH 31/32] fix flake8 --- tests/test_imkar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_imkar.py b/tests/test_imkar.py index f801097..b131299 100644 --- a/tests/test_imkar.py +++ b/tests/test_imkar.py @@ -5,7 +5,7 @@ import pytest -from imkar import imkar +from imkar import imkar # noqa: F401 @pytest.fixture From 08504b3ff612479ca8b46d919b869372ffda2ffe Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Mon, 26 Feb 2024 11:36:37 +0100 Subject: [PATCH 32/32] Revert "Merge branch 'cookiecutter' into new_scattering_freefield" This reverts commit ffaad46540b922d0bb987ade856ad05786009bb7, reversing changes made to 4f98a5b7a4c30189aec3cb7bf8226b5b0a313768. --- .circleci/config.yml | 115 ++++++++++------------ .editorconfig | 3 - .github/ISSUE_TEMPLATE.md | 19 +++- .github/PULL_REQUEST_TEMPLATE.md | 11 +++ .gitignore | 22 ++--- .readthedocs.yml | 4 +- CONTRIBUTING.rst | 164 ++++++++++++------------------- HISTORY.rst | 2 +- LICENSE | 2 +- MANIFEST.in | 1 + Makefile | 84 ++++++++++++++++ README.rst | 14 +-- docs/conf.py | 16 +-- docs/index.rst | 7 -- docs/make.bat | 72 +++++++------- docs/modules.rst | 3 +- docs/modules/imkar.rst | 7 -- docs/resources/pyfar.png | Bin 6300 -> 0 bytes environment.yml | 12 --- imkar/__init__.py | 2 - pytest.ini | 4 - requirements_dev.txt | 7 +- setup.cfg | 6 +- setup.py | 42 ++------ tests/test_imkar.py | 2 +- tox.ini | 37 +++++++ 26 files changed, 332 insertions(+), 326 deletions(-) create mode 100644 Makefile delete mode 100644 docs/modules/imkar.rst delete mode 100644 docs/resources/pyfar.png delete mode 100644 environment.yml delete mode 100644 pytest.ini create mode 100644 tox.ini diff --git a/.circleci/config.yml b/.circleci/config.yml index 42082da..63bc27a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,34 +66,32 @@ jobs: pip-dependency-file: requirements_dev.txt - run: name: Flake8 - command: flake8 imkar tests - - - test_documentation_build: - parameters: - version: - description: "version tag" - default: "latest" - type: string - executor: - name: python-docker - version: <> - - steps: - - checkout - # - run: - # name: Install System Dependencies - # command: sudo apt-get update && sudo apt-get install -y libsndfile1 texlive-latex-extra dvipng - - python/install-packages: - pkg-manager: pip - # app-dir: ~/project/package-directory/ # If you're requirements.txt isn't in the root directory. - pip-dependency-file: requirements_dev.txt - - run: - name: Sphinx - command: | - pip install -e . - cd docs/ - make html SPHINXOPTS="-W" + command: flake8 imkar + +# test_examples: +# parameters: +# version: +# description: "version tag" +# default: "latest" +# type: string +# executor: +# name: python-docker +# version: <> + +# steps: +# - checkout +# # - run: +# # name: Install System Dependencies +# # command: sudo apt-get update && sudo apt-get install -y libsndfile1 +# - python/install-packages: +# pkg-manager: pip +# # app-dir: ~/project/package-directory/ # If you're requirements.txt isn't in the root directory. +# pip-dependency-file: requirements_dev.txt +# - run: +# name: Examples +# command: | +# pip install -e . +# pytest --nbmake examples/*.ipynb test_pypi_publish: parameters: @@ -130,12 +128,9 @@ workflows: matrix: parameters: version: - - "3.8" - - "3.9" - - "3.10" - - "3.11" - - "3.12" - + - "3.8" + - "3.9" + - "3.10" - flake: matrix: parameters: @@ -144,14 +139,13 @@ workflows: requires: - build_and_test - - - test_documentation_build: - matrix: - parameters: - version: - - "3.9" - requires: - - build_and_test + # - test_examples: + # matrix: + # parameters: + # version: + # - "3.9" + # requires: + # - build_and_test test_and_publish: @@ -162,12 +156,9 @@ test_and_publish: matrix: parameters: version: - - "3.8" - - "3.9" - - "3.10" - - "3.11" - - "3.12" - + - "3.8" + - "3.9" + - "3.10" filters: branches: ignore: /.*/ @@ -189,19 +180,19 @@ test_and_publish: tags: only: /^v[0-9]+(\.[0-9]+)*$/ - - test_documentation_build: - matrix: - parameters: - version: - - "3.9" - requires: - - build_and_test - filters: - branches: - ignore: /.*/ - # only act on version tags - tags: - only: /^v[0-9]+(\.[0-9]+)*$/ + # - test_examples: + # matrix: + # parameters: + # version: + # - "3.9" + # requires: + # - build_and_test + # filters: + # branches: + # ignore: /.*/ + # # only act on version tags + # tags: + # only: /^v[0-9]+(\.[0-9]+)*$/ - test_pypi_publish: matrix: @@ -211,7 +202,7 @@ test_and_publish: requires: - build_and_test - flake - - test_documentation_build + # - test_examples filters: branches: ignore: /.*/ diff --git a/.editorconfig b/.editorconfig index 0908478..d4a2c44 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,9 +10,6 @@ insert_final_newline = true charset = utf-8 end_of_line = lf -[*.yml] -indent_size = 2 - [*.bat] indent_style = tab end_of_line = crlf diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index b8e513f..7acd92e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,18 +1,27 @@ -## General - * imkar version: * Python version: * Operating System: -* Did you install pyfar via pip: -## Description +### Description Describe what you were trying to get done. Tell us what happened, what went wrong, and what you expected to happen. -## What I Did +### What I Did ``` Paste the command(s) you ran and the output. If there was a crash, please include the traceback here. ``` + +## Labels + +Label your issue to make it easier for us to assign and track: + +Use one of these labels: +- **hot:** For bugs on the master branch +- **bug:** For bugs not on the master branch +- **enhancement:** For suggesting enhancements of current functionality +- **feature:** For requesting new features +- **documentation:** Everything related to docstrings and comments +- **question:** General questions, e.g., regarding the general structure or future directions diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index dd5be6f..3379f97 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,3 +7,14 @@ Closes # - - - + +### Labels + +Label your issue to make it easier for us to assign and track: + +Use one of these labels: +- **hot:** For bugs on the master branch +- **bug:** For bugs not on the master branch +- **enhancement:** For suggesting enhancements of current functionality +- **feature:** For requesting new features +- **documentation:** Everything related to docstrings and comments diff --git a/.gitignore b/.gitignore index ede6d71..4c915d1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ __pycache__/ # Distribution / packaging .Python +env/ build/ develop-eggs/ dist/ @@ -23,7 +24,6 @@ wheels/ *.egg-info/ .installed.cfg *.egg -MANIFEST # PyInstaller # Usually these files are written by a python script from a template @@ -37,6 +37,7 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ +.tox/ .coverage .coverage.* .cache @@ -53,7 +54,6 @@ coverage.xml # Django stuff: *.log local_settings.py -db.sqlite3 # Flask stuff: instance/ @@ -64,14 +64,12 @@ instance/ # Sphinx documentation docs/_build/ -docs/_autosummary # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints -*/.ipynb_checkpoints/* # pyenv .python-version @@ -82,14 +80,13 @@ celerybeat-schedule # SageMath parsed files *.sage.py -# Environments +# dotenv .env + +# virtualenv .venv -env/ venv/ ENV/ -env.bak/ -venv.bak/ # Spyder project settings .spyderproject @@ -104,13 +101,6 @@ venv.bak/ # mypy .mypy_cache/ -# vs code +# IDE settings .vscode/ .idea/ - -# macOS -.DS_Store - -# workaround for failing test discovery in vscode -tests/*/__init__.py -tests/private/ diff --git a/.readthedocs.yml b/.readthedocs.yml index 099ecc1..9a269eb 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,8 +9,8 @@ build: os: ubuntu-22.04 tools: python: "3.10" - # apt_packages: - # - libsndfile1 + apt_packages: + - libsndfile1 # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 76845c5..8c30f2a 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -7,59 +7,66 @@ Contributing Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. +You can contribute in many ways: + Types of Contributions ---------------------- -Report Bugs or Suggest Features -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Report Bugs +~~~~~~~~~~~ + +Report bugs at https://github.com/pyfar/imkar/issues. -The best place for this is https://github.com/pyfar/imkar/issues. +Fix Bugs +~~~~~~~~ -Fix Bugs or Implement Features -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Look through the GitHub issues for bugs. Anything tagged with "bug" and "help +wanted" is open to whoever wants to implement it. -Look through https://github.com/pyfar/imkar/issues for bugs or feature request -and contact us or comment if you are interested in implementing. +Implement Features +~~~~~~~~~~~~~~~~~~ + +Look through the GitHub issues for features. Anything tagged with "enhancement" +and "help wanted" is open to whoever wants to implement it. Write Documentation ~~~~~~~~~~~~~~~~~~~ -imkar could always use more documentation, whether as part of the +https://github.com/pyfar/imkar could always use more documentation, whether as part of the official imkar docs, in docstrings, or even on the web in blog posts, articles, and such. Get Started! ------------ -Ready to contribute? Here's how to set up `imkar` for local development using the command-line interface. Note that several alternative user interfaces exist, e.g., the Git GUI, `GitHub Desktop `_, extensions in `Visual Studio Code `_ ... +Ready to contribute? Here's how to set up `imkar` for local development. -1. `Fork `_ the `imkar` repo on GitHub. -2. Clone your fork locally and cd into the imkar directory:: +1. Fork the `imkar` repo on GitHub. +2. Clone your fork locally:: - $ git clone https://github.com/YOUR_USERNAME/imkar.git - $ cd imkar + $ git clone https://github.com/pyfar/imkar.git -3. Install your local copy into a virtualenv. Assuming you have Anaconda or Miniconda installed, this is how you set up your fork for local development:: +3. Install your local copy into a virtual environment. Assuming you have Anaconda or Miniconda installed, this is how you set up your fork for local development:: - $ conda create --name imkar python + $ conda create --name imkar python pip $ conda activate imkar - $ conda install pip - $ pip install -e . + $ cd imkar $ pip install -r requirements_dev.txt + $ pip install -e . -4. Create a branch for local development. Indicate the intention of your branch in its respective name (i.e. `feature/branch-name` or `bugfix/branch-name`):: +4. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 5. When you're done making changes, check that your changes pass flake8 and the - tests:: + tests, including testing other Python versions with tox:: $ flake8 imkar tests $ pytest - flake8 test must pass without any warnings for `./imkar` and `./tests` using the default or a stricter configuration. Flake8 ignores `E123/E133, E226` and `E241/E242` by default. If necessary adjust the your flake8 and linting configuration in your IDE accordingly. + To get flake8 and tox, just pip install them into your virtualenv. 6. Commit your changes and push your branch to GitHub:: @@ -67,7 +74,7 @@ Ready to contribute? Here's how to set up `imkar` for local development using th $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature -7. Submit a pull request on the develop branch through the GitHub website. +7. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- @@ -75,76 +82,37 @@ Pull Request Guidelines Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. -2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring. -3. If checks do not pass, have a look at https://app.circleci.com/pipelines/github/pyfar/imkar for more information. - -Function and Class Guidelines ------------------------------ - -Functions and classes should - -* have a single clear purpose and a functionality limited to that purpose. Conditional parameters are fine in some cases but are an indicator that a function or class does not have a clear purpose. Conditional parameters are - - - parameters that are obsolete if another parameter is provided - - parameters that are necessary only if another parameter is provided - - parameters that must have a specific value depending on other parameters - -* be split into multiple functions or classes if the functionality not well limited. -* contain documentation for all input and output parameters. -* contain examples in the documentation if they are non-trivial to use. -* contain comments in the code that explain decisions and parts that are not trivial to read from the code. As a rule of thumb, too much comments are better than to little comments. -* use clear names for all variables - -It is also a good idea to follow `the Zen of Python `_ - -Errors should be raised if - -* Audio objects do not have the correct type (e.g. a TimeData instance is passed but a Signal instance is required) -* String input that specifies a function option has an invalid value (e.g. 'linea' was passed but 'linear' was required) -* Invalid parameter combinations are used - -Warnings should be raised if - -* Results might be wrong or unexpected -* Possibly bad parameter combinations are used +2. If the pull request adds functionality, the docs should be updated. Put + your new functionality into a function with a docstring. +3. Check https://app.circleci.com/pipelines/github/pyfar/imkar + and make sure that the tests pass for all supported Python versions. Testing Guidelines ----------------------- -Pyfar uses test-driven development based on `three steps `_ and `continuous integration `_ to test and monitor the code. -In the following, you'll find a guideline. Note: these instructions are not generally applicable outside of pyfar. +imkar uses test-driven development based on `three steps `_ and `continuous integration `_ to test and monitor the code. +In the following, you'll find a guideline. - The main tool used for testing is `pytest `_. - All tests are located in the *tests/* folder. - Make sure that all important parts of imkar are covered by the tests. This can be checked using *coverage* (see below). - In case of imkar, mainly **state verification** is applied in the tests. This means that the outcome of a function is compared to a desired value (``assert ...``). For more information, it is refered to `Martin Fowler's article `_. -Required Tests -~~~~~~~~~~~~~~ - -The testing should include - -- Test all errors and warnings (see also function and class guidelines above) -- Test all parameters -- Test specific parameter combinations if required -- Test with single and multi-dimensional input data such Signal objects and array likes -- Test with audio objects with complex time data and NaN values (if applicable) - Tips ~~~~~~~~~~~ Pytest provides several, sophisticated functionalities which could reduce the effort of implementing tests. - Similar tests executing the same code with different variables can be `parametrized `_. An example is ``test___eq___differInPoints`` in *test_coordinates.py*. -- Run a single test with +- Run a single test with:: $ pytest tests/test_plot.py::test_line_plots -- Exclude tests (for example the time consuming test of plot) with +- Exclude tests (for example the time consuming test of plot) with:: - $ pytest -k 'not plot and not interaction' + $ pytest -k 'not plot' -- Create an html report on the test `coverage `_ with +- Create an html report on the test `coverage `_ with:: $ pytest --cov=. --cov-report=html @@ -152,7 +120,9 @@ Pytest provides several, sophisticated functionalities which could reduce the ef Fixtures ~~~~~~~~ -"Software test fixtures initialize test functions. They provide a fixed baseline so that tests execute reliably and produce consistent, repeatable, results. Initialization may setup services, state, or other operating environments. These are accessed by test functions through parameters; for each fixture used by a test function there is typically a parameter (named after the fixture) in the test function’s definition." (from https://docs.pytest.org/en/stable/fixture.html) +This section is not specific to imkar, but oftentimes refers to features and examples implemented in the pyfar package which is one of the main dependencies of `imkar `_. + +"Software test fixtures initialize test functions. They provide a fixed baseline so that tests execute reliably and produce consistent, repeatable, results. Initialization may setup services, state, or other operating environments. These are accessed by test functions through arguments; for each fixture used by a test function there is typically a parameter (named after the fixture) in the test function’s definition." (from https://docs.pytest.org/en/stable/fixture.html) - All fixtures are implemented in *conftest.py*, which makes them automatically available to all tests. This prevents from implementing redundant, unreliable code in several test files. - Typical fixtures are imkar objects with varying properties, stubs as well as functions need for initiliazing tests. @@ -163,31 +133,32 @@ Have a look at already implemented fixtures in *confest.py*. **Dummies** If the objects used in the tests have arbitrary properties, tests are usually better to read, when these objects are initialized within the tests. If the initialization requires several operations or the object has non-arbitrary properties, this is a hint to use a fixture. -Good examples illustrating these two cases are the initializations in *test_signal.py* vs. the sine and impulse signal fixtures in *conftest.py*. +Good examples illustrating these two cases are the initializations in pyfar's *test_signal.py* vs. the sine and impulse signal fixtures in pyfar's *conftest.py*. **Stubs** -Stubs mimic actual objects, but have minimum functionality and **fixed, well defined properties**. They are **only used in cases, when a dependence on the actual imkar class is prohibited**. This is the case, when functionalities of the class itself or methods it depends on are tested. Examples are the tests of the Signal class and its methods in *test_signal.py* and *test_fft.py*. +Stubs mimic actual objects, but have minimum functionality and **fixed, well defined properties**. They are **only used in cases, when a dependence on the actual class is prohibited**. +This is the case, when functionalities of the class itself or methods it depends on are tested. Examples are the tests of the pyfar Signal class and its methods in *test_signal.py* and *test_fft.py*. -It requires a little more effort to implement stubs of the imkar classes. Therefore, stub utilities are provided in *imkar/testing/stub_utils.py* and imported in *confest.py*, where the actual stubs are implemented. +It requires a little more effort to implement stubs of classes. Therefore, stub utilities are provided in and imported in *confest.py*, where the actual stubs are implemented. - Note: the stub utilities are not meant to be imported to test files directly or used for other purposes than testing. They solely provide functionality to create fixtures. -- The utilities simplify and harmonize testing within the imkar package and improve the readability and reliability. -- The implementation as the private submodule ``imkar.testing.stub_utils`` further allows the use of similar stubs in related packages with imkar dependency (e.g. other packages from the pyfar} family). +- The utilities simplify and harmonize testing within package and improve the readability and reliability. +- The implementation as the private submodule ``pyfar.testing.stub_utils`` further allows the use of similar stubs in related packages with pyfar dependency (e.g. other packages from the pyfar family). **Mocks** Mocks are similar to stubs but used for **behavioral verification**. For example, a mock can replace a function or an object to check if it is called with correct parameters. A main motivation for using mocks is to avoid complex or time-consuming external dependencies, for example database queries. -- A typical use case of mocks in the imkar context is hardware communication, for example reading and writing of large files or audio in- and output. These use cases are rare compared to tests performing state verification. +- A typical use case of mocks in the pyfar context is hardware communication, for example reading and writing of large files or audio in- and output. These use cases are rare compared to tests performing state verification. - In contrast to some other guidelines on mocks, external dependencies do **not** need to be mocked in general. Failing tests due to changes in external packages are meaningful hints to modify the code. -- Examples of internal mocking can be found in *test_io.py*, indicated by the pytest ``@patch`` calls. +- Examples of internal mocking can be found in pyfar's *test_io.py*, indicated by the pytest ``@patch`` calls. Writing the Documentation ------------------------- -Pyfar follows the `numpy style guide `_ for the docstring. A docstring has to consist at least of +imkar follows the `numpy style guide `_ for the docstring. A docstring has to consist at least of - A short and/or extended summary, - the Parameters section, and @@ -207,50 +178,45 @@ Here are a few tips to make things run smoothly - Use ``[#]_`` and ``.. [#]`` to get automatically numbered footnotes. - Do not use footnotes in the short summary. Only use footnotes in the extended summary if there is a short summary. Otherwise, it messes with the auto-footnotes. - If a method or class takes or returns pyfar objects for example write ``parameter_name : Signal``. This will create a link to the ``pyfar.Signal`` class. -- Plots can be included in by using the prefix ``.. plot::`` followed by an empty line and an indented block containing the code for the plot. See `pyfar.plot.line.time.py` for examples. +- Plots can be included in by using the prefix ``.. plot::`` followed by an empty line and an indented block containing the code for the plot. See the `Sphinx homepage `_ for more information. Building the Documentation -------------------------- -You can build the documentation of your branch using Sphinx by executing the make script inside the docs folder. - -.. code-block:: console +You can build the documentation of your branch using Sphinx by executing the make script inside the docs folder:: $ cd docs/ $ make html -After Sphinx finishes you can open the generated html using any browser - -.. code-block:: console +After Sphinx finishes you can open the generated html using any browser:: $ docs/_build/index.html Note that some warnings are only shown the first time you build the -documentation. To show the warnings again use - -.. code-block:: console +documentation. To show the warnings again use:: $ make clean before building the documentation. + Deploying -~~~~~~~~~ +--------- A reminder for the maintainers on how to deploy. +Make sure all your changes are committed (including an entry in HISTORY.rst). +Then run:: -- Commit all changes to develop -- Update HISTORY.rst in develop -- Merge develop into main - -Switch to main and run:: + $ bump2version patch # possible: major / minor / patch + $ git push + $ git push --tags -$ bumpversion patch # possible: major / minor / patch -$ git push --follow-tags +CircleCI will then deploy to PyPI if tests pass. -The testing platform will then deploy to PyPI if tests pass. +To manually build the package and upload to pypi run:: -- merge main back into develop + $ python setup.py sdist bdist_wheel + $ twine upload dist/* diff --git a/HISTORY.rst b/HISTORY.rst index 917a0b9..009592a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,7 +2,7 @@ History ======= -0.1.0 (2024-02-20) +0.1.0 (2022-09-19) ------------------ * First release on PyPI. diff --git a/LICENSE b/LICENSE index e31563b..c47548d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024, The pyfar developers +Copyright (c) 2022, The pyfar developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index 292d6dd..965b2dd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ +include AUTHORS.rst include CONTRIBUTING.rst include HISTORY.rst include LICENSE diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..235fdff --- /dev/null +++ b/Makefile @@ -0,0 +1,84 @@ +.PHONY: clean clean-build clean-pyc clean-test coverage dist docs help install lint lint/flake8 +.DEFAULT_GOAL := help + +define BROWSER_PYSCRIPT +import os, webbrowser, sys + +from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + +BROWSER := python -c "$$BROWSER_PYSCRIPT" + +help: + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts + +clean-build: ## remove build artifacts + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: ## remove test and coverage artifacts + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + rm -fr .pytest_cache + +lint/flake8: ## check style with flake8 + flake8 imkar tests + +lint: lint/flake8 ## check style + +test: ## run tests quickly with the default Python + pytest + +test-all: ## run tests on every Python version with tox + tox + +coverage: ## check code coverage quickly with the default Python + coverage run --source imkar -m pytest + coverage report -m + coverage html + $(BROWSER) htmlcov/index.html + +docs: ## generate Sphinx HTML documentation, including API docs + $(MAKE) -C docs clean + $(MAKE) -C docs html + $(BROWSER) docs/_build/html/index.html + +servedocs: docs ## compile the docs watching for changes + watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . + +release: dist ## package and upload a release + twine upload dist/* + +dist: clean ## builds source and wheel package + python setup.py sdist + python setup.py bdist_wheel + ls -l dist + +install: clean ## install the package to the active Python's site-packages + python setup.py install diff --git a/README.rst b/README.rst index f2c7e19..7da39b8 100644 --- a/README.rst +++ b/README.rst @@ -6,27 +6,21 @@ imkar .. image:: https://img.shields.io/pypi/v/imkar.svg :target: https://pypi.python.org/pypi/imkar -.. image:: https://img.shields.io/travis/pyfar/imkar.svg - :target: https://travis-ci.com/pyfar/imkar +.. image:: https://img.shields.io/cirrus/github/pyfar/imkar.svg + :target: https://app.circleci.com/pipelines/github/pyfar/imkar .. image:: https://readthedocs.org/projects/imkar/badge/?version=latest :target: https://imkar.readthedocs.io/en/latest/?version=latest :alt: Documentation Status - - A python package for material modeling and quantification in acoustics. - -* Free software: MIT license - - Getting Started =============== Check out `read the docs`_ for the complete documentation. Packages -related to pyfar are listed at `pyfar.org`_. +related to imkar are listed at `pyfar.org`_. Installation ============ @@ -45,6 +39,6 @@ Contributing Refer to the `contribution guidelines`_ for more information. -.. _contribution guidelines: https://github.com/pyfar/imkar/blob/develop/CONTRIBUTING.rst +.. _contribution guidelines: https://github.com/pyfar/imkar/blob/main/CONTRIBUTING.rst .. _pyfar.org: https://pyfar.org .. _read the docs: https://imkar.readthedocs.io/en/latest diff --git a/docs/conf.py b/docs/conf.py index 61c744c..935162e 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ import sys sys.path.insert(0, os.path.abspath('..')) -import imkar +import imkar # noqa # -- General configuration --------------------------------------------- @@ -37,11 +37,9 @@ 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 'sphinx.ext.autosummary', - 'matplotlib.sphinxext.plot_directive', 'sphinx.ext.mathjax', 'sphinx.ext.intersphinx', - 'autodocsumm', - ] + 'autodocsumm'] # show tocs for classes and functions of modules using the autodocsumm # package @@ -65,7 +63,7 @@ # General information about the project. project = 'imkar' -copyright = "2024, The pyfar developers" +copyright = "2022, The pyfar developers" author = "The pyfar developers" # The version info for the project you're documenting, acts as replacement @@ -104,17 +102,10 @@ # intersphinx mapping intersphinx_mapping = { -<<<<<<< HEAD 'numpy': ('https://numpy.org/doc/stable/', None), 'scipy': ('https://docs.scipy.org/doc/scipy/', None), 'matplotlib': ('https://matplotlib.org/stable/', None), 'pyfar': ('https://pyfar.readthedocs.io/en/stable/', None) -======= -'numpy': ('https://numpy.org/doc/stable/', None), -'scipy': ('https://docs.scipy.org/doc/scipy/', None), -'matplotlib': ('https://matplotlib.org/stable/', None), -'pyfar': ('https://pyfar.readthedocs.io/en/stable/', None), ->>>>>>> cookiecutter } # -- Options for HTML output ------------------------------------------- @@ -196,4 +187,3 @@ 'One line description of project.', 'Miscellaneous'), ] - diff --git a/docs/index.rst b/docs/index.rst index fb0146a..3326b76 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,10 +1,3 @@ -.. |pyfar_logo| image:: resources/pyfar.png - :width: 150 - :alt: Alternative text - -|pyfar_logo| - - Getting Started =============== diff --git a/docs/make.bat b/docs/make.bat index e2891c9..29e71ed 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,36 +1,36 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=python -msphinx -) -set SOURCEDIR=. -set BUILDDIR=_build -set SPHINXPROJ=imkar - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The Sphinx module was not found. Make sure you have Sphinx installed, - echo.then set the SPHINXBUILD environment variable to point to the full - echo.path of the 'sphinx-build' executable. Alternatively you may add the - echo.Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=python -msphinx +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=imkar + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The Sphinx module was not found. Make sure you have Sphinx installed, + echo.then set the SPHINXBUILD environment variable to point to the full + echo.path of the 'sphinx-build' executable. Alternatively you may add the + echo.Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/modules.rst b/docs/modules.rst index 33d8645..657f0bd 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1,12 +1,11 @@ Modules ======= -The following gives detailed information about all pyfar functions sorted +The following gives detailed information about all imkar functions sorted according to their modules. .. toctree:: :maxdepth: 1 - modules/imkar modules/imkar.scattering modules/imkar.utils diff --git a/docs/modules/imkar.rst b/docs/modules/imkar.rst deleted file mode 100644 index 0ea5287..0000000 --- a/docs/modules/imkar.rst +++ /dev/null @@ -1,7 +0,0 @@ -imkar -===== - -.. automodule:: imkar - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/resources/pyfar.png b/docs/resources/pyfar.png deleted file mode 100644 index e21a816e8354f3d4923fa27b0a562b8dab50fcc9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6300 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.23.0', + 'pyfar', ] -setup_requirements = [ - 'pytest-runner', -] - -test_requirements = [ - 'pytest', - 'bump2version', - 'wheel', - 'watchdog', - 'flake8', - 'coverage', - 'Sphinx', - 'twine' -] +test_requirements = ['pytest>=3', ] setup( author="The pyfar developers", author_email='info@pyfar.org', + python_requires='>=3.8', classifiers=[ 'Development Status :: 2 - Pre-Alpha', - 'Intended Audience :: Science/Research', + 'Intended Audience :: Scientists', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - ], description="A python package for material modeling and quantification in acoustics.", install_requires=requirements, license="MIT license", - long_description=readme, + long_description=readme + '\n\n' + history, include_package_data=True, keywords='imkar', name='imkar', - packages=find_packages(), - setup_requires=setup_requirements, + packages=find_packages(include=['imkar', 'imkar.*']), test_suite='tests', tests_require=test_requirements, - url="https://pyfar.org/", - download_url="https://pypi.org/project/imkar/", - project_urls={ - "Bug Tracker": "https://github.com/pyfar/imkar/issues", - "Documentation": "https://imkar.readthedocs.io/", - "Source Code": "https://github.com/pyfar/imkar", - }, + url='https://github.com/pyfar/imkar', version='0.1.0', zip_safe=False, - python_requires='>=3.8', ) diff --git a/tests/test_imkar.py b/tests/test_imkar.py index b131299..f801097 100644 --- a/tests/test_imkar.py +++ b/tests/test_imkar.py @@ -5,7 +5,7 @@ import pytest -from imkar import imkar # noqa: F401 +from imkar import imkar @pytest.fixture diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..480bdc2 --- /dev/null +++ b/tox.ini @@ -0,0 +1,37 @@ +[tox] +envlist = py36, py37, py38, flake8 + +[travis] +python = + 3.8: py38 + 3.7: py37 + 3.6: py36 + +[testenv:flake8] +basepython = python +deps = flake8 +commands = flake8 imkar tests + +# Release tooling +[testenv:build] +basepython = python3 +skip_install = true +deps = + wheel + setuptools +commands = + python setup.py -q sdist bdist_wheel + + +[testenv] +setenv = + PYTHONPATH = {toxinidir} +deps = + -r{toxinidir}/requirements_dev.txt +; If you want to make tox run the tests with the same versions, create a +; requirements.txt with the pinned versions and uncomment the following line: +; -r{toxinidir}/requirements.txt +commands = + pip install -U pip + pytest --basetemp={envtmpdir} +