Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pytta/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
from . import utils
from . import rooms
from . import iso3741
from . import iso717

from .functions import read_wav, write_wav, merge, split, \
list_devices, print_devices, get_device_from_user,\
Expand All @@ -130,6 +131,7 @@
'generate',
'utils',
'iso3741',
'iso717',
'rooms',

# Functions
Expand Down
70 changes: 70 additions & 0 deletions pytta/iso717.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
PyTTa ISO 717-1 rating:
----------------------
Single-number quantity of airborne sound insulation, ISO 717-1:2013.

>>> Rw, C, Ctr = pytta.iso717.airborne(R)

"""
import numpy as np

# ISO 717-1:2013, Table 3 (reference values) and Table 4 (spectra No. 1 and
# No. 2 for C and Ctr), from 100 Hz to 3 150 Hz in thirds and from 125 Hz
# to 2 000 Hz in octaves.
_TABLES = {
16: dict(reference=[33, 36, 39, 42, 45, 48, 51, 52, 53, 54, 55, 56, 56,
56, 56, 56],
spectra=([-29, -26, -23, -21, -19, -17, -15, -13, -12, -11, -10,
-9, -9, -9, -9, -9],
[-20, -20, -18, -16, -15, -14, -13, -12, -11, -9, -8,
-9, -10, -11, -13, -15]),
limit=320, at500=7),
5: dict(reference=[36, 45, 52, 55, 56],
spectra=([-21, -14, -8, -5, -4], [-14, -10, -7, -4, -6]),
limit=100, at500=2),
}


def _half_up(x, step):
"""Round to a multiple of step, halves up (ISO 80000-1), clearing the
binary error of the input first."""
return np.floor(np.round(np.asarray(x, dtype=float) / step, 6) + 0.5)


def airborne(R):
"""
Weighted sound reduction index and spectrum adaptation terms of
ISO 717-1:2013, from R, R', Dn or DnT by band.

Parameters:
-----------
* R (array-like):
16 one-third-octave bands from 100 Hz to 3 150 Hz or 5 octave
bands from 125 Hz to 2 000 Hz, in dB.

Return:
-------
* (Xw, C, Ctr), integers in dB: Rw (or R'w, Dn,w, DnT,w) and the
adaptation terms for pink noise and for urban traffic noise.

R is rounded to 0.1 dB, halves up (4.4). The reference curve is shifted
in steps of 1 dB until the sum of unfavourable deviations is as large as
possible but not more than 32.0 dB in thirds or 10.0 dB in octaves, and
Xw is its value at 500 Hz. C and Ctr follow Equations (1) and (2).
Values are kept in tenths of a decibel as integers, so the limit is
compared exactly.
"""
tables = _TABLES.get(len(R))
if tables is None:
raise ValueError('R must have 16 one-third-octave or 5 octave bands.')
tenths = _half_up(R, 0.1)
reference = np.array(tables['reference']) * 10
shift = int(np.floor((tenths - reference).min() / 10)) # no deviation
while np.maximum(reference + (shift + 1) * 10 - tenths, 0).sum() \
<= tables['limit']:
shift += 1
Xw = tables['reference'][tables['at500']] + shift
C = [int(_half_up(-10 * np.log10(np.sum(10 ** ((np.array(L) - tenths / 10)
/ 10))), 1)) - Xw
for L in tables['spectra']]
return Xw, C[0], C[1]
50 changes: 50 additions & 0 deletions tests/test_iso717.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
Tests for pytta.iso717.airborne, the single-number rating of airborne
sound insulation of ISO 717-1:2013. Each test pins one rule.
"""
import os
import unittest

os.environ.setdefault('MPLBACKEND', 'Agg')

import pytta

# ISO 717-1:2013, Annex C, Table C.1: sound reduction index of a building
# element, one-third-octave bands from 100 Hz to 3 150 Hz.
R_ANNEX_C = [20.4, 16.3, 17.7, 22.6, 22.4, 22.7, 24.8, 26.6, 28.0, 30.5, 31.8,
32.5, 33.4, 33.0, 31.0, 25.5]


class TestISO717(unittest.TestCase):

def test_example_of_annex_c(self):
# Rw(C;Ctr) = 30 (-2; -3) dB, with 31.8 dB of unfavourable deviation
self.assertEqual(pytta.iso717.airborne(R_ANNEX_C), (30, -2, -3))

def test_deviation_limit_is_inclusive(self):
# Octave bands: R 5 dB above the reference curve. Shifting it up by
# 7 dB gives 2 dB below R in each of the 5 bands, exactly 10.0 dB,
# which is allowed; 8 dB would give 15 dB.
reference = [36, 45, 52, 55, 56]
Rw, C, Ctr = pytta.iso717.airborne([r + 5 for r in reference])
self.assertEqual(Rw, 52 + 7)

def test_input_is_rounded_half_up_to_one_decimal(self):
# The standard rounds R to 0.1 dB before rating, halves up. These
# five bands end in 5, and round() of Python, which rounds the
# binary value, takes each of them 0.1 dB down: the deviations would
# sum 32.3 dB and Rw would drop to 29. Rounded half up, the curve is
# that of Annex C.
R = list(R_ANNEX_C)
for band, value in ((5, 22.65), (8, 27.95), (9, 30.45), (14, 30.95),
(15, 25.45)):
R[band] = value
self.assertEqual(pytta.iso717.airborne(R), (30, -2, -3))

def test_rejects_other_band_counts(self):
with self.assertRaises(ValueError):
pytta.iso717.airborne(R_ANNEX_C[:-1])


if __name__ == '__main__':
unittest.main()