Directional Wave Spectrum from NDBC Buoys

Some NDBC buoys measure the full directional wave spectrum. A directional wave spectrum \(S(f,\theta)\) contains the sea elevation information over all different directions. This can be used, for instance, to (1) reconstruct a sea surface elevation time-history over some area (as opposed to at just one point), or (2) to calculate the effect of wave loads on a non-axissymetric structure. It is constructed from the omnidirectional spectrum \(S(f)\) and a spread function \(D(f, \theta)\) as

\[S(f, \theta) = S(f)D(f, \theta).\]

NDBC stores this information in 5 parameters, each stored in a different file. One parameter (swden) is simply the omnidirectional spectrum, while the other 4 (swdir, swdir2, swr1, swr2) are used to reconstruct the spread function. For more information on the the parameters and reconstruction see the Spectral Wave Data section in this NDBC site.

In this example we will: * check if a specific buoy has directional information and request that data * reconstruct and plot the directional wave spectrum \(S(f, \theta)\) * take a deeper look at the omnidirectional spectrum \(S(f)\) and spread function \(D(f, \theta)\) separately * calculate and plot the energy spectrum

We use functions from both the wave.io.ndbc and wave.graphics submodules. We start by importing numpy and MHKiT’s wave module.

[1]:
import numpy as np
from mhkit import wave

Request Data

First, we will check if the buoy we’re interested in–42012—has directional data. To do this we use the wave.io.ndbc.available_data function with any one of the 4 parameters used for the spread function. We can see that this buoy does have directional data for the years 2009-2021.

[2]:
buoy = "42012"
wave.io.ndbc.available_data("swdir", buoy)
[2]:
id year filename
312 42012 2009 42012d2009.txt.gz
313 42012 2010 42012d2010.txt.gz
314 42012 2011 42012d2011.txt.gz
315 42012 2012 42012d2012.txt.gz
316 42012 2013 42012d2013.txt.gz
317 42012 2014 42012d2014.txt.gz
318 42012 2015 42012d2015.txt.gz
319 42012 2016 42012d2016.txt.gz
320 42012 2017 42012d2017.txt.gz
321 42012 2018 42012d2018.txt.gz
322 42012 2019 42012d2019.txt.gz
323 42012 2020 42012d2020.txt.gz
324 42012 2021 42012d2021.txt.gz

We will ask for the data for 2021. To do this we use the wave.io.ndbc.request_directional_data function. We see that the spectrum is calculated over ~1hour which results in 8572 different timestamps. We also see there are 47 frequency bins.

[3]:
year = 2021
data_all = wave.io.ndbc.request_directional_data(buoy, year)
data_all
[3]:
<xarray.Dataset>
Dimensions:    (date: 8572, frequency: 47)
Coordinates:
  * date       (date) datetime64[ns] 2021-01-01T00:40:00 ... 2021-12-31T23:40:00
  * frequency  (frequency) float64 0.02 0.0325 0.0375 ... 0.445 0.465 0.485
Data variables:
    swden      (date, frequency) float64 0.0 0.0 0.0 0.0 ... 0.01 0.01 0.02 0.0
    swdir      (date, frequency) float64 194.0 13.0 6.0 ... 173.0 167.0 159.0
    swdir2     (date, frequency) float64 191.0 11.0 9.0 ... 178.0 169.0 106.0
    swr1       (date, frequency) float64 0.16 0.26 0.21 0.41 ... 0.76 0.87 0.63
    swr2       (date, frequency) float64 0.99 0.93 0.9 0.84 ... 0.28 0.67 0.1

Elevation Variance Spectrum

We now use the data we requested and reconstruct the directional elevation variance spectrum. The first step is to decide on the resolution of wave direction. Here we calculate the spectrum every 2 degrees. For the reconstruction we use the wave.io.ndbc.create_directional_spectrum function. You can see the spectrum has the correct shape of 47 frequencies x 180 directions, and the units are indicated in the attributes.

[4]:
date = np.datetime64("2021-02-21T12:40:00")
data = data_all.sel(date=date)
directions = np.arange(0, 360, 2.0)
spectrum = wave.io.ndbc.create_directional_spectrum(data, directions)
spectrum
[4]:
<xarray.DataArray (frequency: 47, direction: 180)>
array([[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, ...,
        0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
       [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, ...,
        0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
       [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, ...,
        0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
       ...,
       [2.21512935e-04, 3.00007246e-04, 3.89537948e-04, ...,
        4.78972678e-05, 9.59294370e-05, 1.53638415e-04],
       [5.05171904e-04, 5.68137407e-04, 6.34119449e-04, ...,
        3.34637188e-04, 3.88407564e-04, 4.45255232e-04],
       [2.26590428e-04, 2.84552135e-04, 3.45964024e-04, ...,
        7.35307830e-05, 1.21080223e-04, 1.72097824e-04]])
Coordinates:
  * frequency  (frequency) float64 0.02 0.0325 0.0375 ... 0.445 0.465 0.485
  * direction  (direction) float64 0.0 2.0 4.0 6.0 ... 352.0 354.0 356.0 358.0
Attributes:
    units:          m^2/Hz/deg
    long_name:      Elevation variance spectrum
    standard_name:  spectrum
    description:    *Elevation variance (m^2)* spectrum (/Hz/deg).

We finally plot the spectrum using the wave.graphics.plot_directional_spectrum function. Notice that there are negative values, which is due to NDBC’s analysis approach which results in negative values far from the mean direction. From this NDBC site: > \(D(f,\theta)\) can take on negative values because of the trigonometric sine and cosine functions. There are several approaches to prevent or deal with the negative values. For more information and discussion of some approaches see: Use of advanced directional wave spectra analysis methods, M. D. Earle, K. E. Steele, and D. W. C. Wang, Ocean Engineering, Volume 26, Issue 12, December 1999, Pages 1421-1434.

[5]:
wave.graphics.plot_directional_spectrum(spectrum)
[5]:
<PolarAxesSubplot: title={'center': 'Elevation Variance Spectrum'}>
_images/directional_waves_9_1.png

We now show a few other ways of plotting this directional spectra. In the first plot below we set a minimum value of \(0.3m^2/Hz\). In the second plot, we plot 3 contour lines with no fill.

[6]:
wave.graphics.plot_directional_spectrum(spectrum, color_level_min=0.3)
[6]:
<PolarAxesSubplot: title={'center': 'Elevation Variance Spectrum'}>
_images/directional_waves_11_1.png
[7]:
wave.graphics.plot_directional_spectrum(
    spectrum, color_level_min=0.3, fill=False, nlevels=4
)
[7]:
<PolarAxesSubplot: title={'center': 'Elevation Variance Spectrum'}>
_images/directional_waves_12_1.png

Components

In this section we will look plot both the omnidirectional spectrum \(S(f)\) and the spread function \(D(f, \theta)\) separately. Remember that the spectrum above was given by \(S(f)D(f, \theta)\).

[8]:
data["swden"].plot()
[8]:
[<matplotlib.lines.Line2D at 0x12e592670>]
_images/directional_waves_14_1.png
[9]:
spread = wave.io.ndbc.create_spread_function(data, directions)
wave.graphics.plot_directional_spectrum(spread, name="Spread", units="1")
[9]:
<PolarAxesSubplot: title={'center': 'Spread Spectrum'}>
_images/directional_waves_15_1.png

Energy Spectrum

Finally, we show how to calculate and plot other spectra. Here we show the energy spectrum, which is proportional to the elevation variance spectrum with a proportionality constant of \(\rho g\). That is, \(S_E(f, \theta) = \rho g S(f, \theta)\). We provide the plotting function additional information so that it displays the correct name and units.

[10]:
rho = 1025  # kg/m^3
g = 9.81  # m/s^2
wave.graphics.plot_directional_spectrum(spectrum * rho * g, name="Energy", units="J")
[10]:
<PolarAxesSubplot: title={'center': 'Energy Spectrum'}>
_images/directional_waves_17_1.png
[ ]: