Simple Analysis of Ship Transits
================================
.. role:: python(code)
:language: python
.. default-literal-role:: python
In this tutorial we will do the basic steps required to analyze the source levels of some ship transits.
.. dropdown:: Initialization code
.. code-block:: python
>>> import uwacan
>>> from pathlib import Path
>>> import plotly.graph_objects as go
>>> data_path = Path(r"D:\ShipMeasurement")
Ship track
----------
Since we will analyze transits of a ship and compute the source level,
we need to know how the ship has traveled.
We load a position track from a file::
>>> track = uwacan.Track.read_blueflow(data_path / "blueflow.csv")
This returns a `uwacan.Track` object, which has many useful methods to select specific portions, calculate distances, etc.
The track can quickly be visualized on a map as::
>>> fig = track.make_figure()
>>> fig.add_trace(track.plot())
.. only:: development
.. code-block:: python
fig = track.make_figure()
fig.add_trace(track.plot())
fig.write_html(
"docs/user/plots/simple_ship_transits_track.html",
include_plotlyjs="cdn",
full_html=False,
default_width="100%",
post_script=""
"window.addEventListener('load', function() {"
" window.dispatchEvent(new Event('resize'));"
"});"
)
.. raw:: html
:file: plots/simple_ship_transits_track.html
Sensors
-------
To calculate ship source levels, we need information about the `~uwacan.sensor`
that were used to measure the sound.
The main information needed is the sensor sensitivity and its location::
>>> sensor = uwacan.sensor(
... "SoundTrap 1",
... sensitivity=-180,
... latitude=57.62325,
... longitude=11.57790,
... depth=40,
... )
In ``uwacan``, sensors always have to be labeled.
Recordings
----------
Recordings are object is responsible for loading time-data from disk,
and performs conversions from the data storage format to physical units::
>>> recording = uwacan.recordings.SoundTrap.read_folder(
... data_path / "SoundTrap 1",
... sensor=sensor,
... time_compensation=7200,
... )
Some common recording types are implemented in `uwacan.recordings`.
In this example, the recording unit was not set up to use UTC, so we include a 2 hour time compensation.
Background noise
----------------
Background noise should also be recorded and compensated for.
A background noise compensation can be created using the `uwacan.background.Background`
class, but it needs a spectrum to work with.
We can create a suitable spectrum by first computing a spectrogram from time data,
then averaging it over time::
>>> time_data = recording.subwindow(center="2023-08-30 12:00:00z", duration=60).time_data()
>>> spectrogram = uwacan.analysis.Spectrogram(
... time_data,
... bands_per_decade=10,
... min_frequency=20,
... max_frequency=40_000,
... )
>>> spectrum = spectrogram.mean("time")
>>> background_noise = uwacan.background.Background(spectrum)
Propagation model
-----------------
To compute the source level from received levels, we also need a propagation model.
There are a couple simple models implemented in `uwacan.propagation`::
>>> propagation_model = uwacan.propagation.SeabedCriticalAngle(
... water_depth=50,
... substrate_compressional_speed=1600,
... speed_of_sound=1503,
... )
>>> track["depth"] = 5
Since this propagation model needs the source depth, we specify the depth in the
position track for the ship.
Filterbank
----------
Finally, we need to specify what type of filterbank should be used to
compute the frequency spectrum from the time data::
>>> filterbank = uwacan.Filterbank(
... frame_overlap=0.5,
... bands_per_decade=100,
... hybrid_resolution=0.2,
... min_frequency=5,
... max_frequency=40e3,
... )
Transits
--------
Our main interest here is to calculate the ship source level from a number
of controlled transits.
A `uwacan.Transit` is a collection of both a `~uwacan.recordings.Recording` and a `~uwacan.Track`::
>>> transit = uwacan.Transit(recording, track)
If we have more than one transit, we need to manually separate them.
This is easiest done by selecting the time of each transit, using some
combination of start, stop, center, and duration::
>>> transits = [
... transit.subwindow(center="2023-08-30 12:20:00z", duration=300),
... transit.subwindow(start="2023-08-30 12:36:00z", stop="2023-08-30 12:41:00z"),
... transit.subwindow(start="2023-08-30 12:58:00z", duration=300),
... transit.subwindow(duration=300, stop="2023-08-30 13:34:00z"),
... ]
The `~uwacan.TimeWindow.subwindow` method is used a lot, and is implemented
on all ``uwacan`` objects where it makes sense to select data over time.
Transits can typically be selected to be much larger than the section
that should be analyzed: the final selection using the closest point of approach is done later.
Source levels
-------------
There are many ways to compute source levels, but the easiest way
is to average the source level spectrogram over some section
chosen around the closest point of approach (CPA) with some pre-set rules::
>>> ship_level = uwacan.analysis.ShipLevel.analyze_transits(
... *transits,
... filterbank=filterbank,
... propagation_model=propagation_model,
... background_noise=background_noise,
... transit_min_angle=30,
... transit_min_duration=30,
... )
This creates a `~uwacan.analysis.ShipLevel` object, which has the source level
and the received level for all transits and segments in the transits.
Analysis
--------
Most of the time, we want the average of all transits and segments, but averaged in different ways::
>>> source_level = ship_level.power_average("segment").level_average("transit").source_level
>>> fig = go.Figure()
>>> fig.add_scatter(x=source_level.frequency, y=source_level)
>>> fig.update_xaxes(type="log", title="Frequency in Hz")
>>> fig.update_yaxes(title="Source level in dB re. (1 μPa m)2/Hz")
>>> fig.show()
.. only:: development
.. code-block:: python
fig.write_html(
"docs/user/plots/simple_ship_transits_source_level.html",
include_plotlyjs="cdn",
full_html=False,
default_width="100%",
post_script=""
"window.addEventListener('load', function() {"
" window.dispatchEvent(new Event('resize'));"
"});"
)
.. raw:: html
:file: plots/simple_ship_transits_source_level.html
Another useful plot is to check the received level compared to the background noise::
>>> fig = go.Figure()
>>> fig.add_scatter(x=background_noise.frequency, y=uwacan.dB(background_noise), name="Background")
>>> for idx, transit in ship_level.power_average("segment").received_level.groupby("transit"):
... fig.add_scatter(x=transit.frequency, y=transit, name=f"Transit {idx}")
>>> fig.update_xaxes(type="log", title="Frequency in Hz")
>>> fig.update_yaxes(title="Received level in dB re. 1 μPa2/Hz")
>>> fig.show()
.. only:: development
.. code-block:: python
fig.write_html(
"docs/user/plots/simple_ship_transits_received_levels.html",
include_plotlyjs="cdn",
full_html=False,
default_width="100%",
post_script=""
"window.addEventListener('load', function() {"
" window.dispatchEvent(new Event('resize'));"
"});"
)
.. raw:: html
:file: plots/simple_ship_transits_received_levels.html