Biomass density estimator

Generates a mask, as a bitmap, that indicates if the biomass density is low enough to obtain reliable in situ TS values.

This is accomplished by:

  • calculating the number of fish in the acoustic sampling volume (Nv, Sawada et al., 1993; see also Gauthier and Rose, 2001)
  • then calculating the ratio of multiple echoes when measuring a single target (M)
  • then testing the resulting values against their respective threshold values (nv_threshold and m_threshold respectively)

List of operands

Operand 1 - Single beam Sv mean (cell statistic)

Operand 2 - Single beam TS mean (cell statistic)

Operand 3 - Single beam Echo density (cell statistic)

See also: About the Cell Statistic operator, the Operator.eval method

Code

"""
Echoview Code Operator source file
========================================================================

Created by Echoview(R) 10.0.218 on Monday, 29 April 2019

See the Echoview help file for Code operator documentation and
examples.

NumPy User Guide: https://docs.scipy.org/doc/numpy/user/
NumPy Reference: https://docs.scipy.org/doc/numpy/Reference/
SciPy Reference Guide: https://docs.scipy.org/doc/scipy/Reference/

Echoview(R) is a registered trademark of Echoview Software Pty Ltd.
"""

# Authorship information
__author__ = "Echoview Software Pty Ltd. 2019."
__disclaimer__ = (
    "This example code is provided AS IS, without warranty of any "
    "kind, express or implied, including but not limited to the "
    "warranties of merchantability, fitness for a particular purpose "
    "and noninfringement. In no event shall Echoview Software Pty Ltd "
    "be liable for any claim, damages or other liability, arising "
    "from, out of or in connection with the use of this example code."
)
__version__ = "1.0"

# System Imports
from typing import List

# Libraries
import echoview as ev
import numpy as np


class Operator(ev.OperatorBase):
    """
    Biomass density estimator (Sawada index)
    ====================================================================

    Generates a mask (bitmap) that indicates if the biomass density is
    low enough to obtain reliable in situ TS values.

    Operands
    ---------------------

    * Operand 1 - Single beam Sv mean (cell statistic)
    * Operand 2 - Single beam TS mean (cell statistic)
    * Operand 3 - Single beam Echo density (cell statistic)

    Notes
    --------------------------

    This is accomplished by calculating the number of fish in the
    acoustic sampling volume (Nv, Sawada et al., 1993; see also
    Gauthier and Rose, 2001) and ratio of multiple echoes when measuring
    a single target (M) and then testing the resulting values against
    their respective threshold values (nv_threshold and m_threshold
    respectively).
    """

    def __init__(self):
        """
        Initialization: Define the M and Nv threshold values for use
        during the calculation.
        """

        self.m_threshold = 0.7
        self.nv_threshold = 0.04

    def result_type(self, input_types: List[ev.MeasurementType]):
        """
        Determines the type of variable from the list of input operand
        types.

        In this case we are generating a mask for singlebeam data,
        therefore we want SINGLE_BEAM_BOOLEAN.
        """

        return ev.MeasurementType.SINGLE_BEAM_BOOLEAN

    def eval(self, inputs: List[ev.OperandInput]):
        """
        Returns a boolean NumPy array representing which samples have a
        low enough biomass density for reliable in situ TS estimates.
        """

        # Get calibration from first operand (Sv mean), assuming this is
        # consistent across all three operands.
        ref_ping = inputs[0].measurement

        # Get PulseDuration in seconds.
        tau = ref_ping.cal('PulseDuration') / 1000.0
        # Get two way beam angle, converted to linear units.
        two_way_ba_lin = 10.0**(ref_ping.cal('TwoWayBeamAngle') / 10.0)
        c = ref_ping.cal('SoundSpeed')

        # Calculate an array of range values, one for each sample of the
        # current ping. This makes use of NumPy's linspace method, which
        # calculates an evenly spaced array between the start and stop
        # ranges of the ping.
        sample_count = ref_ping.data.shape[0]
        sample_thickness = ((ref_ping.stop_range - ref_ping.start_range)
                            / sample_count)
        ranges = np.linspace(ref_ping.start_range + (sample_thickness / 2),
                             ref_ping.stop_range-(sample_thickness / 2),
                             sample_count)

        # Get the ping samples from each of the three input operands,
        # fixing any invalid values (such as nan, inf) as we do so.
        sv_mean = np.ma.fix_invalid(inputs[0].measurement.data)
        ts_mean = np.ma.fix_invalid(inputs[1].measurement.data)
        nec = np.ma.fix_invalid(inputs[2].measurement.data)

        # Convert the Sv and TS mean values to linear units.
        sv_mean = to_linear(sv_mean)
        ts_mean = to_linear(ts_mean)

        # Calculate fish density from Sv and TS mean given echo
        # integrator.
        nei = sv_mean/ts_mean

        # Calculate number of fish per acoustic sampling volume (Nv),
        # sometimes known as the Sawada index.
        # (nv= c*t*equivBA* range^2 * nei)/2
        nv = (c*tau*two_way_ba_lin*np.square(ranges)*nei) / 2

        # Ratio for multiple echoes when measuring a single target.
        M = ((nei - nec) / nei)

        # Calculate the boolean mask to return using NumPy's logical_and
        # method to evaluate samples where nv < nv Threshold and
        # M < M Threshold.
        # Note: Echoview doesn't support nodata for boolean variables so
        # set any masked out values to False.
        return np.logical_and(nv < self.nv_threshold, M < self.m_threshold) \
                 .filled(False)


def to_linear(data: np.ma.MaskedArray, out=None):
    """Converts the specified input to linear values"""

    # Equivalent to 10 ** (data / 10) but more efficient due to reusing
    # the same output array instead of making multiple copies.
    if out is not None and out.shape != data.shape:
        raise ValueError(
            'out must be a NumPy array with the same shape as the input array')

    out = np.divide(data, 10, out=out)
    out = np.power(10, out, out=out)

    return out

See also

Code operator
About the Code operator
Using the Code operator