Skip to content
Snippets Groups Projects
iso.py 10.29 KiB
import numpy as np
from py_wake.site.distance import StraightDistance
from numpy import newaxis as na
from py_wake.utils.grid_interpolator import GridInterpolator


class ISONoiseModel:

    def __init__(self, src_x, src_y, src_h, freqs, sound_power_level, elevation_function=None):
        """ISONoise model based on

        DSF/ISO/DIS 9613-2
        Acoustics – Attenuation of sound during propagation – Part 2:
        Engineering method for the prediction of sound pressure levels outdoors

        DS/ISO 9613-1:1993
        Akustik. Måling og beskrivelse af ekstern støj. Lydudbredelsesdæmpning udendørs. Del 1:
        Metode til beregning af luftabsorption

        The code is a vectorized version of the implementaion made by Camilla Marie Nyborg <cmny@dtu.dk>

        The model models the sound pressure level from a number of sound sources at a number of receivers taking into
        account
        - spherical geometrical spreading
        - ground reflection/absorption
        - atmospheric absorption (DS/ISO 9613-1:1993)


        Parameters
        ----------
        src_x : array_like
            x coordinate of sound sources
        src_y : array_like
            y coordinate of sound sources
        src_h : array_like or float
            height of sound sources (typically wind turbine hub height)
        freqs : array_like
            Frequencies of sound_power_level
        sound_power_level : array_like
            Emitted sound power level, dim=(n_sources, n_freq)

        """

        if elevation_function:
            src_h += elevation_function(src_x, src_y)
        self.elevation_function = elevation_function
        self.src_x, self.src_y = src_x, src_y
        self.src_h = np.zeros_like(src_x) + src_h
        self.n_src = len(src_x)
        self.distance = StraightDistance()
        self.freqs = freqs
        self.sound_power_level = sound_power_level

    def ground_eff(self, ground_distance_ij, distance_ij, ground_type):
        # Ground effects ISO

        freqs_init = np.array([63.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 8000.0])

        src_h_ij = self.src_h[:, na]
        rec_h_ij = self.distance.dst_h_j[na]

        hei_check_ij = 30.0 * (src_h_ij + rec_h_ij)

        q_ij = np.where(ground_distance_ij <= hei_check_ij, 0, 1 - hei_check_ij / ground_distance_ij)

        Gs = Gr = Gm = ground_type

        # for f=63Hz Am =-3*q, for other frequencies Am = -3 * q * (1-Gm)
        fak = np.where(freqs_init == 63.0, 1, (1 - Gm))
        Am_fij = -3.0 * q_ij[na] * fak[:, na, na]

        # 125 Hz
        a_source_ij = 1.5 + 3.0 * np.exp(-0.12 * (src_h_ij - 5.0)**2) * (1.0 - np.exp(-ground_distance_ij / 50.0)) + \
            5.7 * np.exp(-0.09 * src_h_ij**2) * \
            (1.0 - np.exp(-2.8 * (10.0**(-6.0)) * (ground_distance_ij**2)))
        a_rec_ij = 1.5 + 3.0 * np.exp(-0.12 * (rec_h_ij - 5.0)**2) * (1.0 - np.exp(-ground_distance_ij / 50.0)) + \
            5.7 * np.exp(-0.09 * rec_h_ij**2) * (1.0 - np.exp(-2.8 * (10.0**(-6.0)) * (ground_distance_ij**2)))

        # 250 Hz
        b_source_ij = 1.5 + 8.6 * np.exp(-0.09 * src_h_ij**2) * (1.0 - np.exp(-ground_distance_ij / 50.0))
        b_rec_ij = 1.5 + 8.6 * np.exp(-0.09 * rec_h_ij**2) * (1.0 - np.exp(-ground_distance_ij / 50.0))

        # 500 Hz
        c_source_ij = 1.5 + 14.0 * np.exp(-0.46 * src_h_ij**2) * (1.0 - np.exp(-ground_distance_ij / 50.0))
        c_rec_ij = 1.5 + 14.0 * np.exp(-0.46 * rec_h_ij**2) * (1.0 - np.exp(-ground_distance_ij / 50.0))

        # 1000 Hz
        d_source_ij = 1.5 + 5.0 * np.exp(-0.9 * src_h_ij**2) * (1.0 - np.exp(-ground_distance_ij / 50.0))
        d_rec_ij = 1.5 + 5.0 * np.exp(-0.9 * rec_h_ij**2) * (1.0 - np.exp(-ground_distance_ij / 50.0))

        zeros = np.zeros_like(ground_distance_ij)
        As_fij = np.array([zeros - 1.5,  # 63Hz
                           -1.5 + Gs * a_source_ij,  # 125Hz
                           -1.5 + Gs * b_source_ij,  # 250Hz
                           -1.5 + Gs * c_source_ij,  # 500Hz
                           -1.5 + Gs * d_source_ij,  # 1000Hz
                           zeros - 1.5 * (1.0 - Gs),  # 2000Hz
                           zeros - 1.5 * (1.0 - Gs)  # 8000Hz
                           ])

        Ar_fij = np.array([zeros - 1.5,  # 63 Hz
                           -1.5 + Gr * a_rec_ij,  # 125 Hz
                           -1.5 + Gr * b_rec_ij,  # 250 Hz
                           -1.5 + Gr * c_rec_ij,  # 500 Hz
                           -1.5 + Gr * d_rec_ij,  # 1000 Hz
                           zeros - 1.5 * (1.0 - Gr),  # 2000 Hz
                           zeros - 1.5 * (1.0 - Gr)  # 8000 Hz
                           ])

        # Interpolation to other frequencies are actually not following the standard completely.
        ip = GridInterpolator([freqs_init], np.moveaxis([As_fij, Ar_fij, Am_fij], 0, -1))

        # interpolate to freq and sum up As+Ar+Am
        Agr_ijf = np.moveaxis(ip(np.array([self.freqs]).T).sum(-1), 0, -1)

        # Area of sphere: 4pi R^2
        # 10*log_10(4pi) ~ 11
        # 10 * log_10(R^2) = 20 * log_10(R) =
        # reference area = 1.0 m^2
        Adiv_ij = 20.0 * np.log10(distance_ij / 1.0) + 11.0  # The geometrical spreading (divergence)

        ISO_ground_ijf = - Adiv_ij[:, :, na] - Agr_ijf

        return ISO_ground_ijf

    def atmab(self, distance_ij, T0, RH0):
        # Atmospheric absorption
        T_0 = 293.15
        T_01 = 273.16
        T = T0 + 273.15
        p_s0 = 1.0

        psat = p_s0 * 10.0**(-6.8346 * (T_01 / T)**1.261 + 4.6151)
        h = p_s0 * (RH0) * (psat / p_s0)
        F_rO = 1.0 / p_s0 * (24.0 + 4.04 * (10**4.0) * h * (0.02 + h) / (0.391 + h))
        F_rN = 1.0 / p_s0 * (T_0 / T)**(0.5) * (9.0 + 280 * h *
                                                np.exp(-4.17 * ((T_0 / T)**(1.0 / 3.0) - 1.0)))
        alpha_ps = self.freqs**2.0 / p_s0 * (1.84 * (10**(-11)) * (T / T_0)**(0.5) + (T / T_0)**(-5.0 / 2.0) *
                                             (0.01275 * np.exp(-2239.1 / T) / (F_rO + self.freqs**2.0 / F_rO) +
                                              0.1068 * np.exp(-3352 / T) / (F_rN + self.freqs**2.0 / F_rN)))

        ISO_alpha = alpha_ps[na, na] * 20.0 / np.log(10.0) * distance_ij[:, :, na]
        return ISO_alpha

    def transmission_loss(self, rec_x, rec_y, rec_h, ground_type, Temp, RHum):
        # transmission loss = ground effects + atmospheric absorption
        rec_h = np.zeros_like(rec_x) + rec_h
        if self.elevation_function:
            rec_h += self.elevation_function(rec_x, rec_y)
        self.distance.setup(self.src_x, self.src_y, self.src_h, [rec_x, rec_y, rec_h])
        ground_distance_ij = np.sqrt(self.distance.dx_ij**2 + self.distance.dy_ij**2)
        distance_ij = np.sqrt(self.distance.dx_ij**2 + self.distance.dy_ij**2 + self.distance.dh_ij**2)

        atm_abs_ijf = self.atmab(distance_ij, T0=Temp, RH0=RHum)  # The atmospheric absorption term
        ground_eff_ijf = self.ground_eff(ground_distance_ij, distance_ij, ground_type)

        return ground_eff_ijf - atm_abs_ijf  # Delta_SPL

    def __call__(self, rec_x, rec_y, rec_h, Temp, RHum, ground_type):
        """Calculate the sound pressure level at a list of reveicers

        Parameters
        ----------
        rec_x : array_like
            x coordinate, [m], of receivers
        rec_y : array_like
            y coordinate, [m], of receivers
        rec_h : array_like or float
            height, [m], of receivers (typically 2m)
        Temp : float
            Temperature (deg celcius)
        RHum : float
            Relative humidity (%)
        ground_type : float
            Factor describing the amount of ground absorption, 0=hard reflective ground,
            e.g. paving, water, ice and concrete. 1= soft porous absorbing ground,
            e.g. grass, trees, other vegetation and farming land

        Returns
        -------
        total_spl : array_like
            Total sound pressure level at each receiver
        spl : array_like
            Sound pressure level of freqs, dim=(n_receiver, n_freq)
        """

        # Computing total transmission loss
        Delta_SPL_ijf = self.transmission_loss(rec_x, rec_y, rec_h, ground_type, Temp, RHum)
        sound_power_ijxxf = self.sound_power_level[:, na]
        I, J, F = Delta_SPL_ijf.shape
        shape = [I, J] + [1] * (len(sound_power_ijxxf.shape) - len(Delta_SPL_ijf.shape)) + [F]
        Delta_SPL_ijxxf = Delta_SPL_ijf.reshape(shape)

        # Add negative transmission loss to emitted sound and sum over sources to get sound pressure level
        spl_jxxf = 10.0 * np.log10(np.sum(10.0**((self.sound_power_level[:, na] + Delta_SPL_ijxxf) / 10.0), axis=0))

        # Sum over frequencies to get total sound pressure level
        total_spl_jxx = 10.0 * np.log10(np.sum(10.0**(spl_jxxf / 10.0), axis=-1))
        return total_spl_jxx, spl_jxxf


def main():
    if __name__ == '__main__':
        import matplotlib.pyplot as plt
        from py_wake.deficit_models.gaussian import ZongGaussian
        from py_wake.flow_map import XYGrid
        from py_wake.turbulence_models.crespo import CrespoHernandez

        from py_wake.site._site import UniformSite
        from py_wake.examples.data.swt_dd_142_4100_noise.swt_dd_142_4100 import SWT_DD_142_4100
        from py_wake.utils.layouts import rectangle
        from py_wake.utils.plotting import setup_plot

        wt = SWT_DD_142_4100()
        wfm = ZongGaussian(UniformSite(), wt, turbulenceModel=CrespoHernandez())
        x, y = rectangle(5, 5, 5 * wt.diameter())
        sim_res = wfm(x, y, wd=270, ws=8, mode=0)
        nm = sim_res.noise_model()
        ax1, ax2, ax3 = plt.subplots(1, 3, figsize=(16, 4))[1]
        sim_res.flow_map().plot_wake_map(ax=ax1)

        ax1.plot([x[0]], [1000], '.', label='Receiver 1')
        ax1.plot([x[-1]], [1000], '.', label='Receiver 2')
        ax1.legend()
        total_sp_jlk, spl_jlkf = nm(rec_x=[x[0], x[-1]], rec_y=[1000, 1000], rec_h=2, Temp=20, RHum=80, ground_type=0.0)
        ax2.plot(nm.freqs, spl_jlkf[0, 0, 0], label='Receiver 1')
        ax2.plot(nm.freqs, spl_jlkf[1, 0, 0], label='Receiver 2')
        setup_plot(xlabel='Frequency [Hz]', ylabel='Sound pressure level [dB]', ax=ax2)
        plt.tight_layout()

        nmap = sim_res.noise_map(grid=XYGrid(x=np.linspace(-1000, 5000, 100), y=np.linspace(-1000, 1000, 50), h=2))
        nmap['Total sound pressure level'].squeeze().plot(ax=ax3)
        wt.plot(x, y, ax=ax3)

        plt.show()


main()