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/13] 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 d3474419be047b70c8cd4b275f9b6429c051f45a Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 24 Feb 2023 17:49:56 +0100 Subject: [PATCH 02/13] impl diffusion coefficient --- docs/modules.rst | 1 + docs/modules/imkar.diffusion.coefficient.rst | 7 ++ imkar/diffusion/__init__.py | 7 ++ imkar/diffusion/coefficient.py | 75 ++++++++++++++++++++ tests/test_diffusion_coefficient.py | 49 +++++++++++++ 5 files changed, 139 insertions(+) create mode 100644 docs/modules/imkar.diffusion.coefficient.rst create mode 100644 imkar/diffusion/__init__.py create mode 100644 imkar/diffusion/coefficient.py create mode 100644 tests/test_diffusion_coefficient.py diff --git a/docs/modules.rst b/docs/modules.rst index 9da889f..2224120 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -8,3 +8,4 @@ according to their modules. :maxdepth: 1 modules/imkar.scattering.coefficient + modules/imkar.diffusion.coefficient diff --git a/docs/modules/imkar.diffusion.coefficient.rst b/docs/modules/imkar.diffusion.coefficient.rst new file mode 100644 index 0000000..4f7dd48 --- /dev/null +++ b/docs/modules/imkar.diffusion.coefficient.rst @@ -0,0 +1,7 @@ +imkar.diffusion.coefficient +=========================== + +.. automodule:: imkar.diffusion.coefficient + :members: + :undoc-members: + :show-inheritance: diff --git a/imkar/diffusion/__init__.py b/imkar/diffusion/__init__.py new file mode 100644 index 0000000..0c3362f --- /dev/null +++ b/imkar/diffusion/__init__.py @@ -0,0 +1,7 @@ +from .coefficient import ( + freefield, +) + +__all__ = [ + 'freefield', + ] diff --git a/imkar/diffusion/coefficient.py b/imkar/diffusion/coefficient.py new file mode 100644 index 0000000..0f8b911 --- /dev/null +++ b/imkar/diffusion/coefficient.py @@ -0,0 +1,75 @@ +import numpy as np +import pyfar as pf + + +def freefield(sample_pressure, microphone_weights): + """ + Calculate the free-field diffusion coefficient for each incident direction + after ISO 17497-2:2012 [1]_. See :py:func:`random_incidence` + to calculate the random incidence diffusion coefficient. + + + Parameters + ---------- + sample_pressure : pf.FrequencyData + Reflected sound pressure or directivity of the test sample. Its cshape + need to be (..., #microphones). + microphone_weights : 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 + ------- + diffusion_coefficients : FrequencyData + The diffusion coefficient for each plane wave direction. + + + References + ---------- + .. [1] ISO 17497-2:2012, Sound-scattering properties of surfaces. + Part 2: Measurement of the directional diffusion coefficient in a + free field. Geneva, Switzerland: International Organization for + Standards, 2012. + + + Examples + -------- + Calculate free-field diffusion coefficients and then the random incidence + diffusion coefficient. + + >>> import imkar as ik + >>> diffusion_coefficients = ik.diffusion.coefficient.freefield( + >>> sample_pressure, mic_positions.weights) + >>> random_d = ik.scattering.coefficient.random_incidence( + >>> diffusion_coefficients, incident_positions) + """ + # check inputs + if not isinstance(sample_pressure, pf.FrequencyData): + raise ValueError("sample_pressure has to be FrequencyData") + if not isinstance(microphone_weights, np.ndarray): + raise ValueError("weights_microphones have to be a numpy.array") + if not microphone_weights.shape[0] == sample_pressure.cshape[-1]: + raise ValueError( + "the last dimension of sample_pressure need be same as the " + "weights_microphones.shape.") + + # parse angles + N_i = microphone_weights / np.min(microphone_weights) + + # calculate according to Mommertz correlation method Equation (6) + p_sample_abs_sq = np.moveaxis(np.abs(sample_pressure.freq)**2, -1, 0) + + p_sample_sum_sq = np.sum( + p_sample_abs_sq**2 * N_i, axis=-1) + p_sample_sq_sum = np.sum( + p_sample_abs_sq * N_i, axis=-1)**2 + n = np.sum(N_i) + diffusion_array \ + = (p_sample_sq_sum - p_sample_sum_sq) / ((n-1) * p_sample_sum_sq) + diffusion_coefficients = pf.FrequencyData( + np.moveaxis(diffusion_array, 0, -1), + sample_pressure.frequencies) + diffusion_coefficients.comment = 'diffusion coefficients' + + return diffusion_coefficients diff --git a/tests/test_diffusion_coefficient.py b/tests/test_diffusion_coefficient.py new file mode 100644 index 0000000..0556653 --- /dev/null +++ b/tests/test_diffusion_coefficient.py @@ -0,0 +1,49 @@ +import pytest +import numpy as np +import pyfar as pf +from imkar import diffusion +from imkar.testing import stub_utils + + +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +def test_freefield(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) + d = diffusion.coefficient.freefield(p_sample, mics.weights) + np.testing.assert_allclose(d.freq, 1) + + +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +@pytest.mark.parametrize("radius", [ + (1), (10)]) +@pytest.mark.parametrize("magnitude", [ + (1), (10), (np.ones((5, 5)))]) +def test_freefield_with_theta_0(frequencies, radius, magnitude): + mics = pf.samplings.sph_gaussian(42, radius) + 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, magnitude, frequencies) + d = diffusion.coefficient.freefield(p_sample, mics.weights) + np.testing.assert_allclose(d.freq, 1) + + +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +@pytest.mark.parametrize("radius", [ + (1), (10)]) +def test_freefield_not_one(frequencies, radius): + # validate with code from itatoolbox + mics = pf.samplings.sph_equal_angle(10, radius) + mics.weights = pf.samplings.calculate_sph_voronoi_weights(mics) + mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere + theta_is_pi = mics.get_sph().T[1] == np.pi/2 + mics.weights[theta_is_pi] /= 2 + p_sample = stub_utils.frequency_data_from_shape( + mics.cshape, 1, frequencies) + p_sample.freq[1, :] = 2 + d = diffusion.coefficient.freefield(p_sample, mics.weights) + np.testing.assert_allclose(d.freq, 0.9918, atol=0.003) From 6eefe0a8d927b34ac6a4886b2f943b659b2dea3a Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 24 Feb 2023 18:50:51 +0100 Subject: [PATCH 03/13] fix module --- imkar/__init__.py | 1 + 1 file changed, 1 insertion(+) 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 From 42d4366baff8221af25c5af6ac2a83375d8685cf Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 3 Mar 2023 16:15:41 +0100 Subject: [PATCH 04/13] update doc --- imkar/diffusion/coefficient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imkar/diffusion/coefficient.py b/imkar/diffusion/coefficient.py index 0f8b911..335da29 100644 --- a/imkar/diffusion/coefficient.py +++ b/imkar/diffusion/coefficient.py @@ -11,7 +11,7 @@ def freefield(sample_pressure, microphone_weights): Parameters ---------- - sample_pressure : pf.FrequencyData + sample_pressure : pyfar.FrequencyData Reflected sound pressure or directivity of the test sample. Its cshape need to be (..., #microphones). microphone_weights : np.ndarray @@ -21,7 +21,7 @@ def freefield(sample_pressure, microphone_weights): Returns ------- - diffusion_coefficients : FrequencyData + diffusion_coefficients : pyfar.FrequencyData The diffusion coefficient for each plane wave direction. From 58f733a3196faa1680fb021be4b47bf453088ec7 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 3 Mar 2023 16:23:03 +0100 Subject: [PATCH 05/13] fix doc --- docs/conf.py | 4 ++++ imkar/diffusion/coefficient.py | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 63b769a..a99dced 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -99,6 +99,10 @@ # default language for highlighting in source code highlight_language = "python3" +intersphinx_mapping = { + '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/diffusion/coefficient.py b/imkar/diffusion/coefficient.py index 335da29..7e329d3 100644 --- a/imkar/diffusion/coefficient.py +++ b/imkar/diffusion/coefficient.py @@ -11,17 +11,17 @@ def freefield(sample_pressure, microphone_weights): Parameters ---------- - sample_pressure : pyfar.FrequencyData + sample_pressure : :doc:`pf.FrequencyData ` Reflected sound pressure or directivity of the test sample. Its cshape need to be (..., #microphones). - microphone_weights : np.ndarray + microphone_weights : 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 ------- - diffusion_coefficients : pyfar.FrequencyData + diffusion_coefficients : :doc:`pf.FrequencyData ` The diffusion coefficient for each plane wave direction. From 172563afb3c6a09cfb43753384b20dcf830bb049 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 3 Mar 2023 16:26:22 +0100 Subject: [PATCH 06/13] remove scattering stuff --- docs/modules.rst | 1 - docs/modules/imkar.scattering.coefficient.rst | 7 - imkar/__init__.py | 1 - imkar/scattering/__init__.py | 9 - imkar/scattering/coefficient.py | 127 ----------- imkar/testing/__init__.py | 7 - tests/test_scattering_coefficient.py | 198 ------------------ 7 files changed, 350 deletions(-) delete mode 100644 docs/modules/imkar.scattering.coefficient.rst delete mode 100644 imkar/scattering/__init__.py delete mode 100644 imkar/scattering/coefficient.py delete mode 100644 imkar/testing/__init__.py delete mode 100644 tests/test_scattering_coefficient.py diff --git a/docs/modules.rst b/docs/modules.rst index 2224120..f8e53c2 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -7,5 +7,4 @@ according to their modules. .. toctree:: :maxdepth: 1 - modules/imkar.scattering.coefficient modules/imkar.diffusion.coefficient 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/imkar/__init__.py b/imkar/__init__.py index e9d7db7..dd4c696 100644 --- a/imkar/__init__.py +++ b/imkar/__init__.py @@ -5,5 +5,4 @@ __version__ = '0.1.0' -from . import scattering # noqa from . import diffusion # noqa diff --git a/imkar/scattering/__init__.py b/imkar/scattering/__init__.py deleted file mode 100644 index 5e3fc4d..0000000 --- a/imkar/scattering/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .coefficient import ( - random_incidence, - freefield -) - -__all__ = [ - 'freefield', - 'random_incidence' - ] diff --git a/imkar/scattering/coefficient.py b/imkar/scattering/coefficient.py deleted file mode 100644 index 6dbc11f..0000000 --- a/imkar/scattering/coefficient.py +++ /dev/null @@ -1,127 +0,0 @@ -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 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/tests/test_scattering_coefficient.py b/tests/test_scattering_coefficient.py deleted file mode 100644 index 22c9717..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.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) From 321ccfa6af6c5d28ebc55950d9da7dc0e15bc4bf Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 3 Mar 2023 16:28:59 +0100 Subject: [PATCH 07/13] fix flake8 --- imkar/diffusion/coefficient.py | 4 ++-- tests/test_imkar.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/imkar/diffusion/coefficient.py b/imkar/diffusion/coefficient.py index 7e329d3..8d4d342 100644 --- a/imkar/diffusion/coefficient.py +++ b/imkar/diffusion/coefficient.py @@ -11,7 +11,7 @@ def freefield(sample_pressure, microphone_weights): Parameters ---------- - sample_pressure : :doc:`pf.FrequencyData ` + sample_pressure : :doc:`pf.FrequencyData ` # noqa Reflected sound pressure or directivity of the test sample. Its cshape need to be (..., #microphones). microphone_weights : ndarray @@ -21,7 +21,7 @@ def freefield(sample_pressure, microphone_weights): Returns ------- - diffusion_coefficients : :doc:`pf.FrequencyData ` + diffusion_coefficients : :doc:`pf.FrequencyData ` # noqa The diffusion coefficient for each plane wave direction. 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 99c443aba4f80c5fb6ffdd37720e70be02cf37f9 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 3 Mar 2023 16:35:59 +0100 Subject: [PATCH 08/13] fix doc --- docs/conf.py | 4 ---- imkar/diffusion/coefficient.py | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index a99dced..63b769a 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -99,10 +99,6 @@ # default language for highlighting in source code highlight_language = "python3" -intersphinx_mapping = { - '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/diffusion/coefficient.py b/imkar/diffusion/coefficient.py index 8d4d342..ca4ba28 100644 --- a/imkar/diffusion/coefficient.py +++ b/imkar/diffusion/coefficient.py @@ -11,7 +11,7 @@ def freefield(sample_pressure, microphone_weights): Parameters ---------- - sample_pressure : :doc:`pf.FrequencyData ` # noqa + sample_pressure : pyfar.FrequencyData Reflected sound pressure or directivity of the test sample. Its cshape need to be (..., #microphones). microphone_weights : ndarray @@ -21,7 +21,7 @@ def freefield(sample_pressure, microphone_weights): Returns ------- - diffusion_coefficients : :doc:`pf.FrequencyData ` # noqa + diffusion_coefficients : pyfar.FrequencyData The diffusion coefficient for each plane wave direction. From 89695617a4455d297bc47f32cec10d85df44fb35 Mon Sep 17 00:00:00 2001 From: Anne Heimes <64446926+ahms5@users.noreply.github.com> Date: Fri, 10 Mar 2023 15:22:46 +0100 Subject: [PATCH 09/13] add formula to docstring --- imkar/diffusion/coefficient.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/imkar/diffusion/coefficient.py b/imkar/diffusion/coefficient.py index ca4ba28..3e838c0 100644 --- a/imkar/diffusion/coefficient.py +++ b/imkar/diffusion/coefficient.py @@ -3,11 +3,25 @@ def freefield(sample_pressure, microphone_weights): - """ + r""" Calculate the free-field diffusion coefficient for each incident direction after ISO 17497-2:2012 [1]_. See :py:func:`random_incidence` to calculate the random incidence diffusion coefficient. + .. math:: + d(\vartheta_S,\varphi_S) = + \frac{(\sum |\underline{p}_{sample}(\vartheta_R,\varphi_R)| \cdot + N_i)^2 - \sum (|\underline{p}_{sample}(\vartheta_R,\varphi_R)|)^2 + \cdot N_i} + {(\sum N_i - 1) \cdot \sum + (|\underline{p}_{sample}(\vartheta_R,\varphi_R)|)^2 \cdot N_i} + + with + + .. math:: + N_i = \frac{A_i}{A_{min}} + + and ``A`` being the area weights ``microphone_weights``. Parameters ---------- From 1d52864be5ad22c8a975ce37dbf144926c3bc2c0 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Wed, 24 May 2023 09:59:46 -0400 Subject: [PATCH 10/13] add discussion to diffusion --- docs/modules.rst | 2 +- docs/modules/imkar.diffusion.coefficient.rst | 7 - docs/modules/imkar.diffusion.rst | 7 + imkar/__init__.py | 1 + imkar/diffusion/__init__.py | 2 +- .../{coefficient.py => diffusion.py} | 42 ++++++ imkar/scattering/scattering.py | 135 ++++++++++++++++++ imkar/testing/stub_utils.py | 22 --- imkar/utils/__init__.py | 7 + imkar/utils/utils.py | 57 ++++++++ tests/conftest.py | 73 ++++++++++ tests/test_diffusion.py | 45 ++++++ tests/test_diffusion_coefficient.py | 49 ------- tests/test_sub_utils.py | 37 ----- tests/test_utils.py | 34 +++++ 15 files changed, 403 insertions(+), 117 deletions(-) delete mode 100644 docs/modules/imkar.diffusion.coefficient.rst create mode 100644 docs/modules/imkar.diffusion.rst rename imkar/diffusion/{coefficient.py => diffusion.py} (68%) create mode 100644 imkar/scattering/scattering.py delete mode 100644 imkar/testing/stub_utils.py create mode 100644 imkar/utils/__init__.py create mode 100644 imkar/utils/utils.py create mode 100644 tests/conftest.py create mode 100644 tests/test_diffusion.py delete mode 100644 tests/test_diffusion_coefficient.py delete mode 100644 tests/test_sub_utils.py create mode 100644 tests/test_utils.py diff --git a/docs/modules.rst b/docs/modules.rst index f8e53c2..7847e23 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -7,4 +7,4 @@ according to their modules. .. toctree:: :maxdepth: 1 - modules/imkar.diffusion.coefficient + modules/imkar.diffusion diff --git a/docs/modules/imkar.diffusion.coefficient.rst b/docs/modules/imkar.diffusion.coefficient.rst deleted file mode 100644 index 4f7dd48..0000000 --- a/docs/modules/imkar.diffusion.coefficient.rst +++ /dev/null @@ -1,7 +0,0 @@ -imkar.diffusion.coefficient -=========================== - -.. automodule:: imkar.diffusion.coefficient - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/modules/imkar.diffusion.rst b/docs/modules/imkar.diffusion.rst new file mode 100644 index 0000000..6d54d53 --- /dev/null +++ b/docs/modules/imkar.diffusion.rst @@ -0,0 +1,7 @@ +imkar.diffusion +=============== + +.. automodule:: imkar.diffusion + :members: + :undoc-members: + :show-inheritance: diff --git a/imkar/__init__.py b/imkar/__init__.py index dd4c696..8ff2300 100644 --- a/imkar/__init__.py +++ b/imkar/__init__.py @@ -6,3 +6,4 @@ from . import diffusion # noqa +from . import utils # noqa diff --git a/imkar/diffusion/__init__.py b/imkar/diffusion/__init__.py index 0c3362f..4f7e256 100644 --- a/imkar/diffusion/__init__.py +++ b/imkar/diffusion/__init__.py @@ -1,4 +1,4 @@ -from .coefficient import ( +from .diffusion import ( freefield, ) diff --git a/imkar/diffusion/coefficient.py b/imkar/diffusion/diffusion.py similarity index 68% rename from imkar/diffusion/coefficient.py rename to imkar/diffusion/diffusion.py index 3e838c0..96bb392 100644 --- a/imkar/diffusion/coefficient.py +++ b/imkar/diffusion/diffusion.py @@ -1,5 +1,6 @@ import numpy as np import pyfar as pf +from imkar import utils def freefield(sample_pressure, microphone_weights): @@ -87,3 +88,44 @@ def freefield(sample_pressure, microphone_weights): diffusion_coefficients.comment = 'diffusion coefficients' return diffusion_coefficients + + +def random( + random_diffusions, incident_directions): + r""" + Calculate the random-incidence scattering coefficient + according to Paris formula [2]_. + + .. math:: + d_{rand} = \sum d(\vartheta_S,\varphi_S) \cdot cos(\vartheta_S) \cdot w + + with the ``random_diffusions``, and the + area weights ``w`` from the ``incident_directions``. + Note that the incident directions should be + equally distributed to get a valid result. + + Parameters + ---------- + random_diffusions : pyfar.FrequencyData + Diffusion coefficients for different incident directions. Its cshape + needs to be (..., #source_directions) + incident_directions : pyfar.Coordinates + Defines the incidence directions of each `random_diffusions` 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. + + Returns + ------- + random_diffusion : pyfar.FrequencyData + The random-incidence diffusion coefficient. + + References + ---------- + .. [2] H. Kuttruff, Room acoustics, Sixth edition. Boca Raton: + CRC Press/Taylor & Francis Group, 2017. + """ + random_diffusion = utils.paris_formula( + random_diffusions, incident_directions) + random_diffusion.comment = 'random-incidence diffusion coefficient' + return random_diffusion diff --git a/imkar/scattering/scattering.py b/imkar/scattering/scattering.py new file mode 100644 index 0000000..355c64d --- /dev/null +++ b/imkar/scattering/scattering.py @@ -0,0 +1,135 @@ +import numpy as np +import pyfar as pf +from imkar import utils + + +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]_: + + .. 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 + scattering coefficient. + + Parameters + ---------- + sample_pressure : pyfar.FrequencyData + Reflected sound pressure or directivity of the test sample. Its cshape + 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 + `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`. + + Returns + ------- + scattering_coefficients : pyfar.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. + + """ + # check inputs + if not isinstance(sample_pressure, pf.FrequencyData): + raise ValueError( + "sample_pressure has to be a pyfar.FrequencyData object") + if not isinstance(reference_pressure, pf.FrequencyData): + raise ValueError( + "reference_pressure has to be a pyfar.FrequencyData object") + if not isinstance(microphone_weights, np.ndarray): + raise ValueError("microphone_weights have to be a numpy.array") + if sample_pressure.cshape != reference_pressure.cshape: + raise ValueError( + "sample_pressure and reference_pressure have to have the " + "same cshape.") + if microphone_weights.shape[0] != sample_pressure.cshape[-1]: + raise ValueError( + "the last dimension of sample_pressure needs be same as the " + "microphone_weights.shape.") + if not np.allclose( + sample_pressure.frequencies, reference_pressure.frequencies): + raise ValueError( + "sample_pressure and reference_pressure have to have the " + "same frequencies.") + + # calculate according to mommertz correlation method Equation (5) + p_sample = np.moveaxis(sample_pressure.freq, -1, 0) + p_reference = np.moveaxis(reference_pressure.freq, -1, 0) + p_sample_sq = np.abs(p_sample)**2 + p_reference_sq = np.abs(p_reference)**2 + p_cross = p_sample * np.conj(p_reference) + + p_sample_sum = np.sum(microphone_weights * p_sample_sq, axis=-1) + p_ref_sum = np.sum(microphone_weights * p_reference_sq, axis=-1) + p_cross_sum = np.sum(microphone_weights * p_cross, axis=-1) + + data_scattering_coefficient \ + = 1 - ((np.abs(p_cross_sum)**2)/(p_sample_sum*p_ref_sum)) + + scattering_coefficients = pf.FrequencyData( + np.moveaxis(data_scattering_coefficient, 0, -1), + sample_pressure.frequencies) + scattering_coefficients.comment = 'scattering coefficient' + + return scattering_coefficients + + +def random( + scattering_coefficients, incident_directions): + r""" + Calculate the random-incidence scattering coefficient + according to Paris formula [2]_. + + .. math:: + s_{rand} = \sum s(\vartheta_S,\varphi_S) \cdot cos(\vartheta_S) \cdot w + + with the ``scattering_coefficients``, and the + area weights ``w`` from the ``incident_directions``. + Note that the incident directions should be + equally distributed to get a valid result. + + Parameters + ---------- + scattering_coefficients : pyfar.FrequencyData + Scattering coefficients for different incident directions. Its cshape + needs to be (..., #source_directions) + 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. + + 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) + random_scattering.comment = 'random-incidence scattering coefficient' + return random_scattering 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/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..6eb8697 --- /dev/null +++ b/imkar/utils/utils.py @@ -0,0 +1,57 @@ +import numpy as np +import pyfar as pf + + +def paris_formula(coefficients, incident_directions): + r""" + Calculate the random-incidence coefficient + according to Paris formula [2]_. + + .. 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 + needs to be (..., #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 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") + if not isinstance(incident_directions, pf.Coordinates): + raise ValueError("incident_directions have to be None or Coordinates") + if incident_directions.cshape[0] != coefficients.cshape[-1]: + raise ValueError( + "the last dimension of coefficients needs be same as " + "the incident_directions.cshape.") + + theta = incident_directions.get_sph().T[1] + weight = np.cos(theta) * incident_directions.weights + norm = np.sum(weight) + coefficients_freq = np.swapaxes(coefficients.freq, -1, -2) + random_coefficient = pf.FrequencyData( + np.sum(coefficients_freq*weight/norm, axis=-1), + coefficients.frequencies, + comment='random-incidence coefficient' + ) + return random_coefficient diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8a443c9 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,73 @@ +import pytest +import pyfar as pf +import numpy as np + + +@pytest.fixture +def half_sphere(): + """return 42th order gaussian sampling for the half sphere and radius 1. + Returns + ------- + pf.Coordinates + half sphere sampling grid + """ + mics = pf.samplings.sph_gaussian(42) + # delete lower part of sphere + return mics[mics.get_sph().T[1] <= np.pi/2] + + +@pytest.fixture +def quarter_half_sphere(): + """return 10th order gaussian sampling for the quarter half sphere + and radius 1. + Returns + ------- + pf.Coordinates + quarter half sphere sampling grid + """ + incident_directions = pf.samplings.sph_gaussian(10) + incident_directions = incident_directions[ + incident_directions.get_sph().T[1] <= np.pi/2] + return incident_directions[ + incident_directions.get_sph().T[0] <= np.pi/2] + + +@pytest.fixture +def pressure_data_mics(half_sphere): + """returns a sound pressure data example, with sound pressure 0 and + two frequency bins + Parameters + ---------- + half_sphere : pf.Coordinates + half sphere sampling grid for mics + Returns + ------- + pyfar.FrequencyData + output sound pressure data + """ + frequencies = [200, 300] + shape_new = np.append(half_sphere.cshape, len(frequencies)) + return pf.FrequencyData(np.zeros(shape_new), frequencies) + + +@pytest.fixture +def pressure_data_mics_incident_directions( + half_sphere, quarter_half_sphere): + """returns a sound pressure data example, with sound pressure 0 and + two frequency bins + Parameters + ---------- + half_sphere : pf.Coordinates + half sphere sampling grid for mics + quarter_half_sphere : pf.Coordinates + quarter half sphere sampling grid for incident directions + Returns + ------- + pyfar.FrequencyData + output sound pressure data + """ + frequencies = [200, 300] + shape_new = np.append( + quarter_half_sphere.cshape, half_sphere.cshape) + shape_new = np.append(shape_new, len(frequencies)) + return pf.FrequencyData(np.zeros(shape_new), frequencies) diff --git a/tests/test_diffusion.py b/tests/test_diffusion.py new file mode 100644 index 0000000..80e535d --- /dev/null +++ b/tests/test_diffusion.py @@ -0,0 +1,45 @@ +import pytest +import numpy as np +import pyfar as pf +from imkar import diffusion + + +def test_freefield(half_sphere, pressure_data_mics): + mics = half_sphere + p_sample = pressure_data_mics.copy() + p_sample.freq.fill(1) + d = diffusion.freefield(p_sample, mics.weights) + np.testing.assert_allclose(d.freq, 1) + + +@pytest.mark.parametrize("radius", [ + (1), (10)]) +@pytest.mark.parametrize("magnitude", [ + (1), (10)]) +def test_freefield_with_theta_0( + half_sphere, pressure_data_mics, radius, magnitude): + mics = half_sphere + spherical = mics.get_sph().T + mics.set_sph(spherical[0], spherical[1], radius) + p_sample = pressure_data_mics.copy() + p_sample.freq.fill(magnitude) + d = diffusion.freefield(p_sample, mics.weights) + np.testing.assert_allclose(d.freq, 1) + + +@pytest.mark.parametrize("frequencies", [ + ([100, 200]), ([100])]) +@pytest.mark.parametrize("radius", [ + (1), (10)]) +def test_freefield_not_one(frequencies, radius): + # validate with code from itatoolbox + mics = pf.samplings.sph_equal_angle(10, radius) + mics.weights = pf.samplings.calculate_sph_voronoi_weights(mics) + mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere + theta_is_pi = mics.get_sph().T[1] == np.pi/2 + mics.weights[theta_is_pi] /= 2 + data = np.ones(mics.cshape) + p_sample = pf.FrequencyData(data[..., np.newaxis], [100]) + p_sample.freq[1, :] = 2 + d = diffusion.freefield(p_sample, mics.weights) + np.testing.assert_allclose(d.freq, 0.9918, atol=0.003) diff --git a/tests/test_diffusion_coefficient.py b/tests/test_diffusion_coefficient.py deleted file mode 100644 index 0556653..0000000 --- a/tests/test_diffusion_coefficient.py +++ /dev/null @@ -1,49 +0,0 @@ -import pytest -import numpy as np -import pyfar as pf -from imkar import diffusion -from imkar.testing import stub_utils - - -@pytest.mark.parametrize("frequencies", [ - ([100, 200]), ([100])]) -def test_freefield(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) - d = diffusion.coefficient.freefield(p_sample, mics.weights) - np.testing.assert_allclose(d.freq, 1) - - -@pytest.mark.parametrize("frequencies", [ - ([100, 200]), ([100])]) -@pytest.mark.parametrize("radius", [ - (1), (10)]) -@pytest.mark.parametrize("magnitude", [ - (1), (10), (np.ones((5, 5)))]) -def test_freefield_with_theta_0(frequencies, radius, magnitude): - mics = pf.samplings.sph_gaussian(42, radius) - 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, magnitude, frequencies) - d = diffusion.coefficient.freefield(p_sample, mics.weights) - np.testing.assert_allclose(d.freq, 1) - - -@pytest.mark.parametrize("frequencies", [ - ([100, 200]), ([100])]) -@pytest.mark.parametrize("radius", [ - (1), (10)]) -def test_freefield_not_one(frequencies, radius): - # validate with code from itatoolbox - mics = pf.samplings.sph_equal_angle(10, radius) - mics.weights = pf.samplings.calculate_sph_voronoi_weights(mics) - mics = mics[mics.get_sph().T[1] <= np.pi/2] # delete lower part of sphere - theta_is_pi = mics.get_sph().T[1] == np.pi/2 - mics.weights[theta_is_pi] /= 2 - p_sample = stub_utils.frequency_data_from_shape( - mics.cshape, 1, frequencies) - p_sample.freq[1, :] = 2 - d = diffusion.coefficient.freefield(p_sample, mics.weights) - np.testing.assert_allclose(d.freq, 0.9918, atol=0.003) 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) 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 599cd70a82d592d0aff9b458e38a3740e0192053 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 20 Feb 2024 16:32:14 +0100 Subject: [PATCH 11/13] 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 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', ] +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 12/13] 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 6343440f9a50897c28e5b1454cd44bea95d4b257 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Mon, 26 Feb 2024 11:36:46 +0100 Subject: [PATCH 13/13] Revert "Merge branch 'cookiecutter' into new_diffusion_coefficient" This reverts commit d231c5bbc34b447edd8106245526a6ab82e29b6f, reversing changes made to 1d52864be5ad22c8a975ce37dbf144926c3bc2c0. --- .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 | 18 +--- 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 | 43 ++------ tests/test_imkar.py | 2 +- tox.ini | 37 +++++++ 26 files changed, 331 insertions(+), 330 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 012fbc1..63b769a 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,8 @@ '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 +62,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 @@ -102,14 +99,6 @@ # 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 @@ -189,4 +178,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 4318e71..7847e23 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1,11 +1,10 @@ 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.diffusion 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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', ] 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} +