Coverage for src/swiift/api/spectra.py: 100%

1"""Consistant implementations of usual spectral formulations.

2

3 Spectra are defined as function of the frequency. Definitions are taken

4 from [1]_.

5

6 References

7----------

8.. [1] Stansberg, Carl Trygve & Contento, G. & Hong, S.W. & Irani, M. &

9 Ishida, S. & Mercier, R.. (2002). The specialist committee on waves

10 final report and recommendations to the 23rd ITTC. Proceedings of the

11 23rd ITTC. 505-736.

12

13"""

14

15 import abc (empty)

16 import functools (empty)

17 import typing (empty)

18

19 import attrs (empty)

20 import numpy as np (empty)

21 import scipy.integrate as integrate (empty)

22

23 from ..lib.constants import PI_2 (empty)

24 from ..lib.physics import _demote_to_scalar (empty)

25

26"""Constant appearing in the definition of the PM family spectra.""" (empty)

27 _ALPHA = 8.1e-3 (empty)

28"""JONSWAP peak width, below and beyond peak frequency.""" (empty)

29 _PEAK_WIDTH_LEQ = 0.07 (empty)

30 _PEAK_WIDTH_GT = 0.09 (empty)

31

32 T = typing.TypeVar("T", float, np.ndarray[tuple[int], np.dtype[np.floating]]) (empty)

33

34

35 class _ParametrisedSpectrum(abc.ABC): (empty)

36 @abc.abstractmethod (empty)

37 def density(self, frequencies: T) -> T: (empty)

38 """Spectral density as a function of frequency.

39

40 Parameters

41 ----------

42 frequencies : array_like

43 Frequencies at which to evaluate the density.

44

45 Returns

46 -------

47 density: float or np.ndarray

48 The density in m^2 s.

49

50 """

51

52 def discrete_energy(self, frequencies: np.ndarray) -> float: (empty)

53 r"""Spectral density integrated over the input frequencies.

54

55 The full spectral energy is obtained by integrating the

56 density over the positive half-line: this method computes

57 energy for the frequency band spanned by the input array.

58

59 The spectral density is computed for the input frequencies,

60 and integrated over these same frequencies using the

61 trapezoid rule, so that for an input of size :math:`N+1`:

62

63 .. math::

64

65 E(f_0, f_{N-1}) \approx \frac{1}{2} \sum_j=0^{N-1} {

66 (S(f_j) + S(f_{j+1})) (f_{j+1} - f_j)

67 }.

68

69

70 It is assumed the input array is sorted.

71

72 Parameters

73 ----------

74 frequencies : np.ndarray

75 Frequencies to use for computing the energy.

76

77 Returns

78 -------

79 float

80 Wave spectral energy in m^2.

81

82 """

83 return np.trapezoid(self.density(frequencies), frequencies) 1 ctx 1b

84

85 def _density_ang(self, angular_frequency: T) -> T: (empty)

86 # Helper method to use angular frequency as input to the density.

87 return self.density(angular_frequency / PI_2) / PI_2 1 ctx 1b

88

89 def __call__(self, frequencies: T) -> T: (empty)

90 """Wrapper for `density`.

91

92 Parameters

93 ----------

94 frequencies : T

95 Frequencies in Hz.

96

97 Returns

98 -------

99 T

100 [TODO:description]

101

102 """

103 return self.density(frequencies) 2 ctx 1bd

104

105

106@attrs.define (empty)

107 class _UnimodalSpectrum(_ParametrisedSpectrum): (empty)

108 @functools.cached_property (empty)

109 def peak_period(self) -> float: (empty)

110 """Spectral peak (modal) period.

111

112 Returns

113 -------

114 float

115 Peak period, in s.

116

117 """

118 return 1 / self.peak_frequency 1 ctx 1e

119

120 @functools.cached_property (empty)

121 def peak_ang_frequency(self) -> float: (empty)

122 """Spectral peak angular frequency.

123

124 Returns

125 -------

126 float

127 Peak angular frequency, in rad s^-1.

128

129 """

130 return PI_2 * self.peak_frequency 1 ctx 1e

131

132 @property (empty)

133 @abc.abstractmethod (empty)

134 def peak_frequency(self) -> float: (empty)

135 """Peak frequency.

136

137 Returns

138 -------

139 float

140 Peak frequency, in Hz.

141

142 """

143

144

145@attrs.define (empty)

146 class _PMFamily(_UnimodalSpectrum): (empty)

147 r"""Base class for spectra of the Bretschneider family.

148

149 These spectra are of the form

150

151 .. math:: S(f) = \frac{A}{f^5}\exp{\Bigl(-\frac{B}{f^4}\Bigr)}

152

153 with the definitions of the parameters :math:`A` and :math:`B`

154 identifying particular forms. For clarity, we call them

155 respectively `scale` and `exp_scale`.

156

157 In practice, spectra are parameterised by physical variables

158 which can be linked back to these parameters. In particular,

159 the peak frequency is given by

160

161 .. math:: f_p = {\Bigl(\frac{4B}{5}\Bigr)}^{1/4}

162

163 and the significant wave height by

164

165 .. math:: H_s = 2\sqrt{\frac{A}{B}}.

166

167 Attributes

168 ----------

169 scale : float

170 exp_scale : float

171

172 Methods

173 -------

174 __call__

175

176 """

177

178 scale: float (empty)

179 exp_scale: float (empty)

180

181 @functools.cached_property (empty)

182 def swh(self) -> float: (empty)

183 r"""Significant wave height.

184

185 We use the spectral definition,

186

187 .. math:: H_s = 4\sqrt{\int_0^{+\infty} S(f) df}.

188

189 Returns

190 -------

191 float

192 Significant wave height, in m.

193

194 """

195 return 2 * (self.scale / self.exp_scale) ** 0.5 3 ctx 1cbd

196

197 @functools.cached_property (empty)

198 def peak_frequency(self) -> float: (empty)

199 return (4 * self.exp_scale / 5) ** 0.25 4 ctx 1acbe

200

201 def density(self, frequencies): (empty)

202 return self.scale / frequencies**5 * np.exp(-self.exp_scale / frequencies**4) 3 ctx 1abd

203

204

205@attrs.define (empty)

206 class PiersonMoskowitz(_PMFamily): (empty)

207 """Class encapsulating the Pierson--Moskowitz spectrum."""

208

209 @staticmethod (empty)

210 def _make_scale(gravity): (empty)

211 return _ALPHA * (gravity / PI_2**2) ** 2 2 ctx 1ac

212

213 @classmethod (empty)

214 def from_swh(cls, swh: float, gravity: float) -> typing.Self: (empty)

215 """Build a spectrum parameterised by the SWH.

216

217 Parameters

218 ----------

219 swh : float

220 Significant wave height, in m.

221 gravity : float

222 Acceleration of gravity, in m s**-2.

223

224 Returns

225 -------

226 PiersonMoskowitz

227 Initialised spectrum object.

228

229 """

230 scale = cls._make_scale(gravity) 1 ctx 1c

231 exp_scale = 4 * scale / swh**2 1 ctx 1c

232 return cls(scale, exp_scale) 1 ctx 1c

233

234 @classmethod (empty)

235 def from_peak_frequency(cls, peak_frequency: float, gravity: float) -> typing.Self: (empty)

236 """Build a spectrum parameterised by the peak frequency.

237

238 Parameters

239 ----------

240 peak_frequency : float

241 Peak frequency, in Hz.

242 gravity : float

243 Acceleration of gravity, in m s^-2.

244

245 Returns

246 -------

247 PiersonMoskowitz

248 Initialised spectrum object.

249

250 """

251 scale = cls._make_scale(gravity) 2 ctx 1ac

252 exp_scale = 5 / 4 * peak_frequency**4 2 ctx 1ac

253 return cls(scale, exp_scale) 2 ctx 1ac

254

255

256 class Bretschneider(_PMFamily): (empty)

257 """Class encapsulating the Bretschneider spectrum."""

258

259 @classmethod (empty)

260 def from_peak_frequency_swh(cls, peak_frequency: float, swh: float) -> typing.Self: (empty)

261 """Build a spectrum parameterised by peak frequency and SWH.

262

263 Parameters

264 ----------

265 peak_frequency : float

266 Peak frequency, in Hz.

267 swh : float

268 Significant wave height, in m.

269

270 Returns

271 -------

272 Bretschneider

273 Initialised spectrum.

274

275 """

276 scale = 5 * swh**2 * peak_frequency**4 / 16 2 ctx 1ac

277 exp_scale = 5 / 4 * peak_frequency**4 2 ctx 1ac

278 return cls(scale, exp_scale) 2 ctx 1ac

279

280

281@attrs.define (empty)

282 class JONSWAP(_UnimodalSpectrum): (empty)

283 """Class encapsulating the JONSWAP spectrum.

284

285 The JONSWAP spectrum modifies the PM spectrum by introducing a peak

286 enhancement factor.

287

288 Attributes

289 ----------

290 peakedness : float

291 _base_spectrum : PiersonMoskowitz

292

293 """

294

295 peakedness: float (empty)

296 _base_spectrum: PiersonMoskowitz (empty)

297

298 @property (empty)

299 def peak_frequency(self): (empty)

300 """Peak frequency.

301

302 Returns

303 -------

304 float

305 Peak frequency, in m.

306

307 """

308 return self._base_spectrum.peak_frequency 3 ctx 1bed

309

310 @functools.cached_property (empty)

311 def swh(self): (empty)

312 r"""Significant wave height.

313

314 The SWH is computed with a cubic approximation.

315

316 Returns

317 -------

318 float

319 Significant wave height, in m.

320

321 """

322 return ( 1 ctx 1d

323 self._base_spectrum.scale**0.5

324 * (

325 1.555

326 + 0.2596 * self.peakedness

327 - 0.02231 * self.peakedness**2

328 + 0.001142 * self.peakedness**3

329 )

330 / self.peak_frequency**2

331 )

332

333 @classmethod (empty)

334 def from_parameters( (empty)

335 cls,

336 peak_frequency: float,

337 peakedness: float,

338 gravity: float,

339 ) -> typing.Self:

340 """Build a spectrum.

341

342 Peak frequency and gravity are used to instantiate the

343 underlying Pierson--Moskowitz spectrum, while peakedness is

344 specific to the JONSWAP formulation.

345

346 Parameters

347 ----------

348 peak_frequency : float

349 Peak frequency, in Hz.

350 peakedness : float

351 Peakedness (base of the peak enhancement factor).

352 gravity : float

353 Acceleration of gravity, in m s^-2.

354

355 Returns

356 -------

357 JONSWAP

358 Initialised spectrum.

359

360 """

361 pm_spectrum = PiersonMoskowitz.from_peak_frequency(peak_frequency, gravity) (empty)

362 return cls(peakedness, pm_spectrum) (empty)

363

364 # Decoratar actually mandatory for integrate.quad not to error out

365 # on this method.

366 @_demote_to_scalar 3 ctx 1abd

367 def density(self, frequencies): 3 ctx 1abd

368 # The dimension of `frequencies` cannot be 0,

369 # to allow for masking/assignement

370 peak_width = np.ones_like(np.atleast_1d(frequencies)) * _PEAK_WIDTH_LEQ 2 ctx 1bd

371 peak_width[frequencies > self.peak_frequency] = _PEAK_WIDTH_GT 2 ctx 1bd

372 peak_enhancement = self.peakedness ** np.exp( 2 ctx 1bd

373 -((frequencies - self.peak_frequency) ** 2)

374 / (2 * peak_width**2 * self.peak_frequency**2)

375 )

376 return self._base_spectrum(frequencies) * peak_enhancement 2 ctx 1bd