1"""Collection of underwater noise source models.
2
3These source models typically return levels in dB,
4but not consistently of the same type.
5See the documentation of each model to know if it is a
6ship level (monopole or radiated noise) or an environmental level.
7Since the models can be evaluated at arbitrary frequencies, they should
8all be power spectral density levels.
9
10.. autosummary::
11 :toctree: generated
12
13 bureau_veritas_advanced
14 bureau_veritas_controlled
15 wales_heitmeyer
16 jomopans_echo
17
18"""
19
20import numpy as np
21import xarray as xr
22
23
24def class_limit_curve(frequency, breakpoints, limits):
25 """Evaluate a class limit curve defined by frequency breakpoints and limits.
26
27 With N breakpoints, there are N+1 regions and corresponding limits.
28
29 Parameters
30 ----------
31 frequency : array_like
32 Input frequency data.
33 breakpoints : list of float
34 The frequency breakpoints, in ascending order.
35 limits : list with the limits
36 Each limit is either a callable which takes the frequency as the input,
37 a single value valid within the entire range, or an array_like with the
38 same shape as frequency.
39
40 Returns
41 -------
42 array_like
43 Array of the same shape as `frequency`.
44
45 Examples
46 --------
47 >>> class_limit_curve(
48 ... np.array([50, 100, 200, 400, 800, 1600]),
49 ... [150, 800],
50 ... [10, 20, 30],
51 ... )
52 [10, 10, 20, 20, 30, 30]
53 """
54 conditions = [frequency < b for b in breakpoints] + [np.full(np.shape(frequency), True)]
55 limits = [limit(frequency) if callable(limit) else limit for limit in limits]
56 levels = np.select(conditions, limits)
57 try:
58 wrapper = frequency.__array_wrap__
59 except AttributeError:
60 return levels
61 else:
62 return wrapper(levels)
63
64
[docs]
65def bureau_veritas_advanced(frequency=None):
66 """Calculate the advanced vessel limit from Bureau Veritas.
67
68 This ship level is a radiated noise level, as a spectral density level.
69 """
70 if frequency is None:
71 frequency = 10 ** (np.arange(10, 48) / 10) # Decidecade bands from 10 Hz to 50 kHz
72 frequency = xr.DataArray(frequency, coords={"frequency": frequency})
73 return class_limit_curve(
74 frequency=frequency,
75 breakpoints=[50, 1e3],
76 limits=[
77 lambda f: 174 - 11 * np.log10(f),
78 lambda f: 155.3 - 18 * np.log10(f / 50),
79 lambda f: 131.9 - 22 * np.log10(f / 1000),
80 ],
81 )
82
83
[docs]
84def bureau_veritas_controlled(frequency=None):
85 """Calculate the controlled vessel limit from Bureau Veritas.
86
87 This ship level is a radiated noise level, as a spectral density level.
88 """
89 if frequency is None:
90 frequency = 10 ** (np.arange(10, 48) / 10) # Decidecade bands from 10 Hz to 50 kHz
91 frequency = xr.DataArray(frequency, coords={"frequency": frequency})
92 return class_limit_curve(
93 frequency=frequency,
94 breakpoints=[50, 1e3],
95 limits=[
96 lambda f: 169 - 2 * np.log10(f),
97 lambda f: 165.6 - 20 * np.log10(f / 50),
98 lambda f: 139.6 - 20 * np.log10(f / 1000),
99 ],
100 )
101
102def dnv_silence_transit(frequency = None):
103 """Calculate the environmental transit limit from DNV.
104
105 This ship level is a radiated noise level, as a spectral density level.
106 """
107 if frequency is None:
108 frequency = 10 ** (np.arange(10, 48) / 10) # Decidecade bands from 10 Hz to 50 kHz
109 frequency = xr.DataArray(frequency, coords={"frequency": frequency})
110 return class_limit_curve(
111 frequency=frequency,
112 breakpoints=[1000],
113 limits=[
114 lambda f: 183 - 5 * np.log10(f),
115 lambda f: 168 - 12 * np.log10(f / 1000),
116 ],
117 ) - 10*np.log10(frequency * 0.231)
118
119
120
[docs]
121def wales_heitmeyer(frequency):
122 """Calculate the Wales-Heitmeyer source model.
123
124 Note that this is a monopole source level model,
125 with unknown validity for the source depths.
126
127 Returns
128 -------
129 L : array_like
130 The calculated source level, as a spectral density level.
131 """
132 return 230 - 10 * np.log10(frequency**3.594) + 10 * np.log10((1 + (frequency / 340) ** 2) ** 0.917)
133
134
[docs]
135def jomopans_echo(frequency, ship_class, speed, length):
136 """Calculate Jomopans-ECHO source model.
137
138 Make sure to use the correct units for speed and length.
139 Note that this is a monopole source level model, assuming
140 a source depth of 6 meters.
141
142 Parameters
143 ----------
144 frequency : array_like
145 The frequencies at which to evaluate
146 ship_class : str
147 Which ship class to use.
148 speed : float
149 The ship speed, in knots
150 length : float
151 The ship length, in meters
152
153
154 Returns
155 -------
156 L : array_like
157 The calculated source level, as a spectral density level.
158 """
159 K = 191
160 K_lf = 208
161
162 D = 3
163 match ship_class:
164 case "fishing":
165 v_class = 6.4
166 case "tug":
167 v_class = 3.7
168 case "naval":
169 v_class = 11.1
170 case "recreational":
171 v_class = 10.6
172 case "research":
173 v_class = 8.0
174 case "cruise":
175 v_class = 17.1
176 D = 4
177 case "passenger":
178 v_class = 9.7
179 case "bulker":
180 v_class = 13.9
181 D_lf = 0.8
182 case "container":
183 v_class = 18
184 D_lf = 0.8
185 case "vehicle":
186 v_class = 15.8
187 D_lf = 1
188 case "tanker":
189 v_class = 12.4
190 D_lf = 1
191 case "other":
192 v_class = 7.4
193 case "dredger":
194 v_class = 9.5
195 case _:
196 raise ValueError(f"Unknown ship class '{ship_class}'")
197
198 f1 = 480 / v_class
199
200 baseline = K - 20 * np.log10(f1) - 10 * np.log10((1 - frequency / f1) ** 2 + D**2)
201 if ship_class in {"container", "vehicle", "bulker", "tanker"}:
202 f_lf = 600 / v_class
203 lf_baseline = (
204 K_lf
205 - 40 * np.log10(f_lf)
206 + 10 * np.log10(frequency)
207 - 10 * np.log10((1 - (frequency / f_lf) ** 2) ** 2 + D_lf**2)
208 )
209 baseline = xr.where(frequency < 100, lf_baseline, baseline)
210 l = length * 3.28084 / 300
211 return baseline + 60 * np.log10(speed / v_class) + 20 * np.log10(l)