Пример #1
0
    def derive(self,
               alt_rad=P('Altitude Radio'),
               alt_agl=P('Altitude AGL'),
               gog=M('Gear On Ground'),
               rtr=S('Rotors Turning')):
        # When was the gear in the air?
        gear_off_grounds = runs_of_ones(gog.array == 'Air')

        if alt_rad and alt_agl and rtr:
            # We can do a full analysis.
            # First, confirm that the rotors were turning at this time:
            gear_off_grounds = slices_and(gear_off_grounds, rtr.get_slices())

            # When did the radio altimeters indicate airborne?
            airs = slices_remove_small_gaps(
                np.ma.clump_unmasked(
                    np.ma.masked_less_equal(alt_agl.array, 1.0)),
                time_limit=AIRBORNE_THRESHOLD_TIME_RW,
                hz=alt_agl.frequency)
            # Both is a reliable indication of being in the air.
            for air in airs:
                for goff in gear_off_grounds:
                    # Providing they relate to each other :o)
                    if slices_overlap(air, goff):
                        start_index = max(air.start, goff.start)
                        end_index = min(air.stop, goff.stop)

                        better_begin = index_at_value(
                            alt_rad.array,
                            1.0,
                            _slice=slice(
                                max(start_index - 5 * alt_rad.frequency, 0),
                                start_index + 5 * alt_rad.frequency))
                        if better_begin:
                            begin = better_begin
                        else:
                            begin = start_index

                        better_end = index_at_value(
                            alt_rad.array,
                            1.0,
                            _slice=slice(
                                max(end_index + 5 * alt_rad.frequency, 0),
                                end_index - 5 * alt_rad.frequency, -1))
                        if better_end:
                            end = better_end
                        else:
                            end = end_index

                        duration = end - begin
                        if (duration /
                                alt_rad.hz) > AIRBORNE_THRESHOLD_TIME_RW:
                            self.create_phase(slice(begin, end))
        else:
            # During data validation we can select just sensible flights;
            # short hops make parameter validation tricky!
            self.create_phases(
                slices_remove_small_gaps(
                    slices_remove_small_slices(gear_off_grounds,
                                               time_limit=30)))
Пример #2
0
    def derive(self, alt_rad=P('Altitude Radio'),
               alt_aal=P('Altitude AAL'),
               alt_baro=P('Altitude STD Smoothed'),
               gog=M('Gear On Ground')):

        # If we have no Altitude Radio we will have to fall back to Altitude AAL
        if not alt_rad:
            self.array = alt_aal.array
            return

        # When was the helicopter on the ground?
        gear_on_grounds = np.ma.clump_masked(np.ma.masked_equal(gog.array, 1))
        # Find and eliminate short spikes (15 seconds) as these are most likely errors.
        short_spikes = slices_find_small_slices(gear_on_grounds, time_limit=20, hz=gog.hz)
        for _slice in short_spikes:
            gog.array[_slice.start:_slice.stop] = 0

        # Remove slices shorter than 15 seconds as these are most likely created in error.
        gear_on_grounds = slices_remove_small_slices(gear_on_grounds, time_limit=20, hz=gog.hz)
        # Compute the half period which we will need.
        hp = int(alt_rad.frequency*ALTITUDE_AGL_SMOOTHING) // 2

        # We force altitude AGL to be zero when the gear shows 'Ground' state
        alt_agl = moving_average(np.maximum(alt_rad.array, 0.0) * (1 - gog.array.data), window=hp*2+1, weightings=None)

        # Refine the baro estimates
        length = len(alt_agl)-1
        baro_sections = np.ma.clump_masked(np.ma.masked_greater(alt_agl, ALTITUDE_AGL_TRANS_ALT))
        for baro_section in baro_sections:
            begin = max(baro_section.start - 1, 0)
            end = min(baro_section.stop + 1, length)
            start_diff = alt_baro.array[begin] - alt_agl[begin]
            stop_diff = alt_baro.array[end] - alt_agl[end]
            if start_diff is not np.ma.masked and stop_diff is not np.ma.masked:
                diff = np.linspace(start_diff, stop_diff, end-begin-2)
                alt_agl[begin+1:end-1] = alt_baro.array[begin+1:end-1]-diff
            elif start_diff is not np.ma.masked:
                alt_agl[begin+1:end-1] = alt_baro.array[begin+1:end-1] - start_diff
            elif stop_diff is not np.ma.masked:
                alt_agl[begin+1:end-1] = alt_baro.array[begin+1:end-1] - stop_diff
            else:
                pass
        low_sections = np.ma.clump_unmasked(np.ma.masked_greater(alt_agl, 5))
        for both in slices_and(low_sections, gear_on_grounds):
            alt_agl[both] = 0.0

        '''
        # Quick visual check of the altitude agl.
        import matplotlib.pyplot as plt
        plt.plot(alt_baro.array, 'y-')
        plt.plot(alt_rad.array, 'r-')
        plt.plot(alt_agl, 'b-')
        plt.show()
        '''

        self.array = alt_agl
    def derive(self, alt_rad=P('Altitude Radio'),
               alt_aal=P('Altitude AAL'),
               alt_baro=P('Altitude STD Smoothed'),
               gog=M('Gear On Ground')):

        # If we have no Altitude Radio we will have to fall back to Altitude AAL
        if not alt_rad:
            self.array = alt_aal.array
            return

        # When was the helicopter on the ground?
        gear_on_grounds = np.ma.clump_masked(np.ma.masked_equal(gog.array, 1))
        # Find and eliminate short spikes (15 seconds) as these are most likely errors.
        short_spikes = slices_find_small_slices(gear_on_grounds, time_limit=15, hz=gog.hz)
        for slice in short_spikes:
            gog.array[slice.start:slice.stop] = 0

        # Remove slices shorter than 15 seconds as these are most likely created in error.
        gear_on_grounds = slices_remove_small_slices(gear_on_grounds, time_limit=15, hz=gog.hz)
        # Compute the half period which we will need.
        hp = int(alt_rad.frequency*ALTITUDE_AGL_SMOOTHING)//2
        # We force altitude AGL to be zero when the gear shows 'Ground' state
        alt_agl = moving_average(np.maximum(alt_rad.array, 0.0) * (1 - gog.array.data), window=hp*2+1, weightings=None)

        # Refine the baro estimates
        length = len(alt_agl)-1
        baro_sections = np.ma.clump_masked(np.ma.masked_greater(alt_agl, ALTITUDE_AGL_TRANS_ALT))
        for baro_section in baro_sections:
            begin = max(baro_section.start - 1, 0)
            end = min(baro_section.stop + 1, length)
            start_diff = alt_baro.array[begin] - alt_agl[begin]
            stop_diff = alt_baro.array[end] - alt_agl[end]
            if start_diff is not np.ma.masked and stop_diff is not np.ma.masked:
                diff = np.linspace(start_diff, stop_diff, end-begin-2)
                alt_agl[begin+1:end-1] = alt_baro.array[begin+1:end-1]-diff
            elif start_diff is not np.ma.masked:
                alt_agl[begin+1:end-1] = alt_baro.array[begin+1:end-1] - start_diff
            elif stop_diff is not np.ma.masked:
                alt_agl[begin+1:end-1] = alt_baro.array[begin+1:end-1] - stop_diff
            else:
                pass
        low_sections = np.ma.clump_unmasked(np.ma.masked_greater(alt_agl, 5))
        for both in slices_and(low_sections, gear_on_grounds):
            alt_agl[both] = 0.0

        '''
        # Quick visual check of the altitude agl.
        import matplotlib.pyplot as plt
        plt.plot(alt_baro.array, 'y-')
        plt.plot(alt_rad.array, 'r-')
        plt.plot(alt_agl, 'b-')
        plt.show()
        '''

        self.array = alt_agl
    def derive(self,
               alt_rad=P('Altitude Radio'),
               alt_agl=P('Altitude AGL'),
               gog=M('Gear On Ground'),
               rtr=S('Rotors Turning')):
        # When was the gear in the air?
        gear_off_grounds = runs_of_ones(gog.array == 'Air')

        if alt_rad and alt_agl and rtr:
            # We can do a full analysis.
            # First, confirm that the rotors were turning at this time:
            gear_off_grounds = slices_and(gear_off_grounds, rtr.get_slices())

            # When did the radio altimeters indicate airborne?
            airs = slices_remove_small_gaps(
                np.ma.clump_unmasked(np.ma.masked_less_equal(alt_agl.array, 1.0)),
                time_limit=AIRBORNE_THRESHOLD_TIME_RW, hz=alt_agl.frequency)
            # Both is a reliable indication of being in the air.
            for air in airs:
                for goff in gear_off_grounds:
                    # Providing they relate to each other :o)
                    if slices_overlap(air, goff):
                        start_index = max(air.start, goff.start)
                        end_index = min(air.stop, goff.stop)

                        better_begin = index_at_value(
                            alt_rad.array, 1.0,
                            _slice=slice(max(start_index-5*alt_rad.frequency, 0),
                                         start_index+5*alt_rad.frequency)
                        )
                        if better_begin:
                            begin = better_begin
                        else:
                            begin = start_index

                        better_end = index_at_value(
                            alt_rad.array, 1.0,
                            _slice=slice(max(end_index+5*alt_rad.frequency, 0),
                                         end_index-5*alt_rad.frequency, -1))
                        if better_end:
                            end = better_end
                        else:
                            end = end_index

                        duration = end - begin
                        if (duration / alt_rad.hz) > AIRBORNE_THRESHOLD_TIME_RW:
                            self.create_phase(slice(begin, end))
        else:
            # During data validation we can select just sensible flights;
            # short hops make parameter validation tricky!
            self.create_phases(
                slices_remove_small_gaps(
                    slices_remove_small_slices(gear_off_grounds, time_limit=30)))
def _split_on_eng_params(slice_start_secs, slice_stop_secs, split_params_min,
                         split_params_frequency):
    '''
    Find split using engine parameters.

    :param slice_start_secs: Start of slow slice in seconds.
    :type slice_start_secs: int or float
    :param slice_stop_secs: Stop of slow slice in seconds.
    :type slice_stop_secs: int or float
    :param split_params_min: Minimum of engine parameters.
    :type split_params_min: np.ma.MaskedArray
    :param split_params_frequency: Frequency of split_params_min.
    :type split_params_frequency: int or float
    :returns: Split index in seconds and value of split_params_min at this
        index.
    :rtype: (int or float, int or float)
    '''
    slice_start = slice_start_secs * split_params_frequency
    slice_stop = slice_stop_secs * split_params_frequency
    split_params_slice = slice(np.round(slice_start, 0),
                               np.round(slice_stop, 0))
    split_index, split_value = min_value(split_params_min,
                                         _slice=split_params_slice)

    if split_index is None:
        return split_index, split_value

    eng_min_slices = slices_remove_small_slices(slices_remove_small_gaps(
        runs_of_ones(split_params_min[split_params_slice] == split_value),
        time_limit=60,
        hz=split_params_frequency),
                                                hz=split_params_frequency)

    if not eng_min_slices:
        return split_index, split_value

    split_index = eng_min_slices[0].start + \
        ((eng_min_slices[0].stop - eng_min_slices[0].start) / 2) + slice_start
    split_index = round(split_index / split_params_frequency)
    return split_index, split_value
def _segment_type_and_slice(speed_array, speed_frequency, heading_array,
                            heading_frequency, start, stop, eng_arrays,
                            aircraft_info, thresholds, hdf):
    """
    Uses the Heading to determine whether the aircraft moved about at all and
    the airspeed to determine if it was a full or partial flight.

    NO_MOVEMENT: When the aircraft is in the hanger,
    the altitude and airspeed can be tested and record values which look like
    the aircraft is in flight; however the aircraft is rarely moved and the
    heading sensor is a good indication that this is a hanger test.

    GROUND_ONLY: If the heading changed, the airspeed needs to have been above
    the threshold speed for flight for a minimum amount of time, currently 3
    minutes to determine. If not, this segment is identified as GROUND_ONLY,
    probably taxiing, repositioning on the ground or a rejected takeoff.

    START_ONLY: If the airspeed started slow but ended fast, we had a partial
    segment for the start of a flight.

    STOP_ONLY:  If the airspeed started fast but ended slow, we had a partial
    segment for the end of a flight.

    MID_FLIGHT: The airspeed started and ended fast - no takeoff or landing!

    START_AND_STOP: The airspeed started and ended slow, implying a complete
    flight.

    segment_type is one of:
    * 'NO_MOVEMENT' (didn't change heading)
    * 'GROUND_ONLY' (didn't go fast)
    * 'START_AND_STOP'
    * 'START_ONLY'
    * 'STOP_ONLY'
    * 'MID_FLIGHT'
    """

    speed_start = start * speed_frequency
    speed_stop = stop * speed_frequency
    speed_array = speed_array[speed_start:speed_stop]

    heading_start = start * heading_frequency
    heading_stop = stop * heading_frequency
    heading_array = heading_array[heading_start:heading_stop]

    # remove small gaps between valid data, e.g. brief data spikes
    unmasked_slices = slices_remove_small_gaps(
        np.ma.clump_unmasked(speed_array), 2, speed_frequency)
    # remove small slices to find 'consistent' valid data
    unmasked_slices = slices_remove_small_slices(unmasked_slices, 40,
                                                 speed_frequency)

    if unmasked_slices:
        # Check speed
        slow_start = speed_array[
            unmasked_slices[0].start] < thresholds['speed_threshold']
        slow_stop = speed_array[unmasked_slices[-1].stop -
                                1] < thresholds['speed_threshold']
        threshold_exceedance = np.ma.sum(
            speed_array > thresholds['speed_threshold']) / speed_frequency
        fast_for_long = threshold_exceedance > thresholds['min_duration']
    else:
        slow_start = slow_stop = fast_for_long = None

    # Find out if the aircraft moved
    if aircraft_info and aircraft_info['Aircraft Type'] == 'helicopter':
        # if any gear params use them
        gog = next(
            iter([
                hdf.get(name)
                for name in ('Gear On Ground', 'Gear (R) On Ground',
                             'Gear (L) On Ground')
            ]))
        if gog:
            gog_start_idx = start * gog.frequency
            gog_stop_idx = stop * gog.frequency
            gog_samples = 120 * gog.frequency
            gog_start = closest_unmasked_value(gog.array, gog_start_idx,
                                               gog_start_idx - gog_samples,
                                               gog_start_idx + gog_samples)
            gog_stop = closest_unmasked_value(gog.array, gog_stop_idx,
                                              gog_stop_idx - gog_samples,
                                              gog_stop_idx + gog_samples)
            if gog_start is not None and gog_stop is not None:
                # Use Gear on Ground rather than rotor speed as rotors may be
                # 90+% at beginning or end of segment.
                slow_start = (gog_start.value == 'Ground')
                slow_stop = (gog_stop.value == 'Ground')
            temp = np.ma.array(gog.array[gog_start_idx:gog_stop_idx].data,
                               mask=gog.array[gog_start_idx:gog_stop_idx].mask)
            gog_test = np.ma.masked_less(temp, 1.0)
            # We have seeen 12-second spurious gog='Air' signals during rotor rundown. Hence increased limit.
            did_move = slices_remove_small_slices(np.ma.clump_masked(gog_test),
                                                  time_limit=30,
                                                  hz=gog.frequency)
        else:
            hdiff = np.ma.abs(np.ma.diff(heading_array)).sum()
            did_move = hdiff > settings.HEADING_CHANGE_TAXI_THRESHOLD
    else:
        # Check Heading change for fixed wing.
        if eng_arrays is not None:
            heading_array = np.ma.masked_where(
                eng_arrays[heading_start:heading_stop] <
                settings.MIN_FAN_RUNNING, heading_array)
        hdiff = np.ma.abs(np.ma.diff(heading_array)).sum()
        did_move = hdiff > settings.HEADING_CHANGE_TAXI_THRESHOLD

    if not did_move or (not fast_for_long and eng_arrays is None):
        # added check for not fast for long and no engine params to avoid
        # lots of Herc ground runs
        logger.debug("Aircraft did not move.")
        segment_type = 'NO_MOVEMENT'
        # e.g. hanger tests, esp. if speed changes!
    elif slow_start and slow_stop and fast_for_long:
        logger.debug(
            "speed started below threshold, rose above and stopped below.")
        segment_type = 'START_AND_STOP'
    elif slow_start and threshold_exceedance:
        logger.debug("speed started below threshold and stopped above.")
        segment_type = 'START_ONLY'
    elif slow_stop and threshold_exceedance:
        logger.debug("speed started above threshold and stopped below.")
        segment_type = 'STOP_ONLY'
    elif not fast_for_long:
        logger.debug("speed was below threshold.")
        segment_type = 'GROUND_ONLY'  # e.g. RTO, re-positioning A/C
        #Q: report a go_fast?
    else:
        logger.debug("speed started and stopped above threshold.")
        segment_type = 'MID_FLIGHT'
    logger.info("Segment type is '%s' between '%s' and '%s'.", segment_type,
                start, stop)

    # ARINC 717 data has frames or superframes. ARINC 767 will be split
    # on a minimum boundary of 4 seconds for the analyser.
    boundary = 64 if hdf.superframe_present else 4
    segment = slice(start, stop)

    supf_start_secs, supf_stop_secs, array_start_secs, array_stop_secs = segment_boundaries(
        segment, boundary)

    start_padding = segment.start - supf_start_secs

    return segment_type, segment, array_start_secs
def split_segments(hdf, aircraft_info):
    '''
    TODO: DJ suggested not to use decaying engine oil temperature.

    Notes:
     * We do not want to split on masked superframe data if mid-flight (e.g.
       short section of corrupt data) - repair_mask without defining
       repair_duration should fix that.
     * Use turning alongside engine parameters to ensure there is no movement?
     XXX: Beware of pre-masked minimums to ensure we don't split on padded
     superframes

    TODO: Use L3UQAR num power ups for difficult cases?
    '''

    segments = []
    speed, thresholds = _get_speed_parameter(hdf, aircraft_info)

    # Look for heading first
    try:
        # Fetch Heading if available
        heading = hdf.get_param('Heading', valid_only=True)
    except KeyError:
        # try Heading True, otherwise fail loudly with a KeyError
        heading = hdf.get_param('Heading True', valid_only=True)

    eng_arrays, _ = _get_eng_params(hdf, align_param=heading)

    # Look for speed
    try:
        speed_array = repair_mask(speed.array,
                                  repair_duration=None,
                                  repair_above=thresholds['speed_threshold'])
    except ValueError:
        # speed array is masked, most likely under min threshold so it did
        # not go fast.
        logger.warning("speed is entirely masked. The entire contents of "
                       "the data will be a GROUND_ONLY slice.")
        return [
            _segment_type_and_slice(speed.array, speed.frequency,
                                    heading.array, heading.frequency, 0,
                                    hdf.duration, eng_arrays, aircraft_info,
                                    thresholds, hdf)
        ]

    speed_secs = len(speed_array) / speed.frequency

    # if Segment Split parameter is in hdf file someone has already done the hard work for us
    if 'Segment Split' in hdf:
        seg_split = hdf['Segment Split']
        split_flags = np.ma.where(seg_split.array == 'Split')
        start = 0

        if split_flags:
            for split_idx in split_flags[0]:
                split_idx = split_idx / seg_split.frequency
                segments.append(
                    _segment_type_and_slice(speed_array, speed.frequency,
                                            heading.array, heading.frequency,
                                            start, split_idx, eng_arrays,
                                            aircraft_info, thresholds, hdf))
                start = split_idx
                logger.info("Split Flag found at at index '%d'.", split_idx)
            # Add remaining data to a segment.
            segments.append(
                _segment_type_and_slice(speed_array, speed.frequency,
                                        heading.array, heading.frequency,
                                        start, speed_secs, eng_arrays,
                                        aircraft_info, thresholds, hdf))
        else:
            # if no split flags use whole file.
            logger.info(
                "'Segment Split' found but no Splits found, using whole file.")
            segments.append(
                _segment_type_and_slice(speed_array, speed.frequency,
                                        heading.array, heading.frequency,
                                        start, speed_secs, eng_arrays,
                                        aircraft_info, thresholds, hdf))
        return segments

    slow_array = np.ma.masked_less_equal(speed_array,
                                         thresholds['speed_threshold'])

    speedy_slices = np.ma.clump_unmasked(slow_array)
    if len(speedy_slices) <= 1:
        logger.info(
            "There are '%d' sections of data where speed is "
            "above the splitting threshold. Therefore there can only "
            "be at maximum one flights worth of data. Creating a "
            "single segment comprising all data.", len(speedy_slices))
        # Use the first and last available unmasked values to determine segment
        # type.
        return [
            _segment_type_and_slice(speed_array, speed.frequency,
                                    heading.array, heading.frequency, 0,
                                    speed_secs, eng_arrays, aircraft_info,
                                    thresholds, hdf)
        ]

    # suppress transient changes in speed around 80 kts
    slow_slices = slices_remove_small_slices(np.ma.clump_masked(slow_array),
                                             10, speed.frequency)

    rate_of_turn = _rate_of_turn(heading)

    split_params_min, split_params_frequency \
        = _get_normalised_split_params(hdf)

    if hdf.reliable_frame_counter:
        dfc = hdf['Frame Counter']
        dfc_diff = np.ma.diff(dfc.array)
        # Mask 'Frame Counter' incrementing by 1.
        dfc_diff = np.ma.masked_equal(dfc_diff, 1)
        # Mask 'Frame Counter' overflow where the Frame Counter transitions
        # from 4095 to 0.
        # Q: This used to be 4094, are there some Frame Counters which
        # increment from 1 rather than 0 or something else?
        dfc_diff = np.ma.masked_equal(dfc_diff, -4095)
        # Gap between difference values.
        dfc_half_period = (1 / dfc.frequency) / 2
    else:
        logger.info("'Frame Counter' will not be used for splitting since "
                    "'reliable_frame_counter' is False.")
        dfc = None

    start = 0
    last_fast_index = None
    for slow_slice in slow_slices:
        if slow_slice.start == 0:
            # Do not split if slow_slice is at the beginning of the data.
            # Since we are working with masked slices, masked padded superframe
            # data will be included within the first slow_slice.
            continue
        if slow_slice.stop == len(speed_array):
            # After the loop we will add the remaining data to a segment.
            break

        if last_fast_index is not None:
            fast_duration = (slow_slice.start -
                             last_fast_index) / speed.frequency
            if fast_duration < settings.MINIMUM_FAST_DURATION:
                logger.info("Disregarding short period of fast speed %s",
                            fast_duration)
                continue

        # Get start and stop at 1Hz.
        slice_start_secs = slow_slice.start / speed.frequency
        slice_stop_secs = slow_slice.stop / speed.frequency

        slow_duration = slice_stop_secs - slice_start_secs
        if slow_duration < thresholds['min_split_duration']:
            logger.info(
                "Disregarding period of speed below '%s' "
                "since '%s' is shorter than MINIMUM_SPLIT_DURATION "
                "('%s').", thresholds['speed_threshold'], slow_duration,
                thresholds['min_split_duration'])
            continue

        last_fast_index = slow_slice.stop

        # Find split based on minimum of engine parameters.
        if split_params_min is not None:
            eng_split_index, eng_split_value = _split_on_eng_params(
                slice_start_secs, slice_stop_secs, split_params_min,
                split_params_frequency)
        else:
            eng_split_index, eng_split_value = None, None

        # Split using 'Frame Counter'.
        if dfc is not None:
            dfc_split_index = _split_on_dfc(slice_start_secs,
                                            slice_stop_secs,
                                            dfc.frequency,
                                            dfc_half_period,
                                            dfc_diff,
                                            eng_split_index=eng_split_index)
            if dfc_split_index:
                segments.append(
                    _segment_type_and_slice(speed_array, speed.frequency,
                                            heading.array, heading.frequency,
                                            start, dfc_split_index, eng_arrays,
                                            aircraft_info, thresholds, hdf))
                start = dfc_split_index
                logger.info(
                    "'Frame Counter' jumped within slow_slice '%s' "
                    "at index '%d'.", slow_slice, dfc_split_index)
                continue
            else:
                logger.info(
                    "'Frame Counter' did not jump within slow_slice "
                    "'%s'.", slow_slice)

        # Split using minimum of engine parameters.
        if eng_split_value is not None and \
           eng_split_value < settings.MINIMUM_SPLIT_PARAM_VALUE:
            logger.info(
                "Minimum of normalised split parameters ('%s') was "
                "below  ('%s') within "
                "slow_slice '%s' at index '%d'.", eng_split_value,
                settings.MINIMUM_SPLIT_PARAM_VALUE, slow_slice,
                eng_split_index)
            segments.append(
                _segment_type_and_slice(speed_array, speed.frequency,
                                        heading.array, heading.frequency,
                                        start, eng_split_index, eng_arrays,
                                        aircraft_info, thresholds, hdf))
            start = eng_split_index
            continue
        else:
            logger.info(
                "Minimum of normalised split parameters ('%s') was "
                "not below MINIMUM_SPLIT_PARAM_VALUE ('%s') within "
                "slow_slice '%s' at index '%s'.", eng_split_value,
                settings.MINIMUM_SPLIT_PARAM_VALUE, slow_slice,
                eng_split_index)

        # Split using rate of turn. Q: Should this be considered in other
        # splitting methods.
        if rate_of_turn is None:
            continue

        rot_split_index = _split_on_rot(slice_start_secs, slice_stop_secs,
                                        heading.frequency, rate_of_turn)
        if rot_split_index:
            segments.append(
                _segment_type_and_slice(speed_array, speed.frequency,
                                        heading.array, heading.frequency,
                                        start, rot_split_index, eng_arrays,
                                        aircraft_info, thresholds, hdf))
            start = rot_split_index
            logger.info(
                "Splitting at index '%s' where rate of turn was below "
                "'%s'.", rot_split_index,
                settings.HEADING_RATE_SPLITTING_THRESHOLD)
            continue
        else:
            logger.info(
                "Aircraft did not stop turning during slow_slice "
                "('%s'). Therefore a split will not be made.", slow_slice)

        #Q: Raise error here?
        logger.warning(
            "Splitting methods failed to split within slow_slice "
            "'%s'.", slow_slice)

    # Add remaining data to a segment.
    segments.append(
        _segment_type_and_slice(speed_array, speed.frequency, heading.array,
                                heading.frequency, start, speed_secs,
                                eng_arrays, aircraft_info, thresholds, hdf))
    '''
    import matplotlib.pyplot as plt
    for look in [speed_array, heading.array, dfc.array, eng_arrays]:
        plt.plot(np.linspace(0, speed_secs, len(look)), look/np.ptp(look))
    for seg in segments:
        plt.plot([seg[1].start, seg[1].stop], [-0.5,+1])
    plt.show()
    '''

    return segments
def _segment_type_and_slice(speed_array, speed_frequency,
                            heading_array, heading_frequency,
                            start, stop, eng_arrays,
                            aircraft_info, thresholds, hdf):
    """
    Uses the Heading to determine whether the aircraft moved about at all and
    the airspeed to determine if it was a full or partial flight.

    NO_MOVEMENT: When the aircraft is in the hanger,
    the altitude and airspeed can be tested and record values which look like
    the aircraft is in flight; however the aircraft is rarely moved and the
    heading sensor is a good indication that this is a hanger test.

    GROUND_ONLY: If the heading changed, the airspeed needs to have been above
    the threshold speed for flight for a minimum amount of time, currently 3
    minutes to determine. If not, this segment is identified as GROUND_ONLY,
    probably taxiing, repositioning on the ground or a rejected takeoff.

    START_ONLY: If the airspeed started slow but ended fast, we had a partial
    segment for the start of a flight.

    STOP_ONLY:  If the airspeed started fast but ended slow, we had a partial
    segment for the end of a flight.

    MID_FLIGHT: The airspeed started and ended fast - no takeoff or landing!

    START_AND_STOP: The airspeed started and ended slow, implying a complete
    flight.

    segment_type is one of:
    * 'NO_MOVEMENT' (didn't change heading)
    * 'GROUND_ONLY' (didn't go fast)
    * 'START_AND_STOP'
    * 'START_ONLY'
    * 'STOP_ONLY'
    * 'MID_FLIGHT'
    """

    speed_start = start * speed_frequency
    speed_stop = stop * speed_frequency
    speed_array = speed_array[speed_start:speed_stop]

    heading_start = start * heading_frequency
    heading_stop = stop * heading_frequency
    heading_array = heading_array[heading_start:heading_stop]

    # remove small slices to find 'consistent' valid data
    unmasked_slices = slices_remove_small_slices(
        np.ma.clump_unmasked(speed_array), 10, speed_frequency)

    if unmasked_slices:
        # Check speed
        slow_start = speed_array[unmasked_slices[0].start] < thresholds['speed_threshold']
        slow_stop = speed_array[unmasked_slices[-1].stop - 1] < thresholds['speed_threshold']
        threshold_exceedance = np.ma.sum(
            speed_array > thresholds['speed_threshold']) / speed_frequency
        fast_for_long = threshold_exceedance > thresholds['min_duration']
    else:
        slow_start = slow_stop = fast_for_long = None

    # Find out if the aircraft moved
    if aircraft_info and aircraft_info['Aircraft Type'] == 'helicopter':
        try:
            gog = hdf['Gear On Ground']
        except:
            gog = hdf['Gear (R) On Ground']
        gog_start = start * gog.frequency
        gog_stop = stop * gog.frequency
        temp = np.ma.array(gog.array[gog_start:gog_stop].data, mask=gog.array[gog_start:gog_stop].mask)
        gog_test = np.ma.masked_less(temp, 1.0)
        # We have seeen 12-second spurious gog='Air' signals during rotor rundown. Hence increased limit.
        did_move = slices_remove_small_slices(np.ma.clump_masked(gog_test),
                                              time_limit=30, hz=gog.frequency)
    else:
        # Check Heading change for fixed wing.
        if eng_arrays is not None:
            heading_array = np.ma.masked_where(eng_arrays[heading_start:heading_stop] < settings.MIN_FAN_RUNNING, heading_array)
        hdiff = np.ma.abs(np.ma.diff(heading_array)).sum()
        did_move = hdiff > settings.HEADING_CHANGE_TAXI_THRESHOLD

    if not did_move or (not fast_for_long and eng_arrays is None):
        # added check for not fast for long and no engine params to avoid
        # lots of Herc ground runs
        logger.debug("Aircraft did not move.")
        segment_type = 'NO_MOVEMENT'
        # e.g. hanger tests, esp. if speed changes!
    elif not fast_for_long:
        logger.debug("speed was below threshold.")
        segment_type = 'GROUND_ONLY'  # e.g. RTO, re-positioning A/C
        #Q: report a go_fast?
    elif slow_start and slow_stop:
        logger.debug(
            "speed started below threshold, rose above and stopped below.")
        segment_type = 'START_AND_STOP'
    elif slow_start:
        logger.debug("speed started below threshold and stopped above.")
        segment_type = 'START_ONLY'
    elif slow_stop:
        logger.debug("speed started above threshold and stopped below.")
        segment_type = 'STOP_ONLY'
    else:
        logger.debug("speed started and stopped above threshold.")
        segment_type = 'MID_FLIGHT'
    logger.info("Segment type is '%s' between '%s' and '%s'.",
                segment_type, start, stop)
    return segment_type, slice(start, stop)
def split_segments(hdf, aircraft_info):
    '''
    TODO: DJ suggested not to use decaying engine oil temperature.

    Notes:
     * We do not want to split on masked superframe data if mid-flight (e.g.
       short section of corrupt data) - repair_mask without defining
       repair_duration should fix that.
     * Use turning alongside engine parameters to ensure there is no movement?
     XXX: Beware of pre-masked minimums to ensure we don't split on padded
     superframes

    TODO: Use L3UQAR num power ups for difficult cases?
    '''

    speed, thresholds = _get_speed_parameter(hdf, aircraft_info)

    # Look for heading first
    try:
        # Fetch Heading if available
        heading = hdf.get_param('Heading', valid_only=True)
    except KeyError:
        # try Heading True, otherwise fail loudly with a KeyError
        heading = hdf.get_param('Heading True', valid_only=True)

    eng_arrays, _ = _get_eng_params(hdf, align_param=heading)

    # Look for speed
    try:
        speed_array = repair_mask(speed.array, repair_duration=None,
                                  repair_above=thresholds['speed_threshold'])
    except ValueError:
        # speed array is masked, most likely under min threshold so it did
        # not go fast.
        logger.warning("speed is entirely masked. The entire contents of "
                       "the data will be a GROUND_ONLY slice.")
        return [_segment_type_and_slice(
            speed.array, speed.frequency, heading.array,
            heading.frequency, 0, hdf.duration, eng_arrays,
            aircraft_info, thresholds, hdf)]

    speed_secs = len(speed_array) / speed.frequency
    slow_array = np.ma.masked_less_equal(speed_array,
                                         thresholds['speed_threshold'])

    speedy_slices = np.ma.clump_unmasked(slow_array)
    if len(speedy_slices) <= 1:
        logger.info("There are '%d' sections of data where speed is "
                    "above the splitting threshold. Therefore there can only "
                    "be at maximum one flights worth of data. Creating a "
                    "single segment comprising all data.", len(speedy_slices))
        # Use the first and last available unmasked values to determine segment
        # type.
        return [_segment_type_and_slice(
            speed_array, speed.frequency, heading.array,
            heading.frequency, 0, speed_secs, eng_arrays,
            aircraft_info, thresholds, hdf)]

    # suppress transient changes in speed around 80 kts
    slow_slices = slices_remove_small_slices(np.ma.clump_masked(slow_array))

    rate_of_turn = _rate_of_turn(heading)

    split_params_min, split_params_frequency \
        = _get_normalised_split_params(hdf)

    if hdf.reliable_frame_counter:
        dfc = hdf['Frame Counter']
        dfc_diff = np.ma.diff(dfc.array)
        # Mask 'Frame Counter' incrementing by 1.
        dfc_diff = np.ma.masked_equal(dfc_diff, 1)
        # Mask 'Frame Counter' overflow where the Frame Counter transitions
        # from 4095 to 0.
        # Q: This used to be 4094, are there some Frame Counters which
        # increment from 1 rather than 0 or something else?
        dfc_diff = np.ma.masked_equal(dfc_diff, -4095)
        # Gap between difference values.
        dfc_half_period = (1 / dfc.frequency) / 2
    else:
        logger.info("'Frame Counter' will not be used for splitting since "
                    "'reliable_frame_counter' is False.")
        dfc = None

    segments = []
    start = 0
    last_fast_index = None
    for slow_slice in slow_slices:
        if slow_slice.start == 0:
            # Do not split if slow_slice is at the beginning of the data.
            # Since we are working with masked slices, masked padded superframe
            # data will be included within the first slow_slice.
            continue
        if slow_slice.stop == len(speed_array):
            # After the loop we will add the remaining data to a segment.
            break

        if last_fast_index is not None:
            fast_duration = (slow_slice.start -
                             last_fast_index) / speed.frequency
            if fast_duration < settings.MINIMUM_FAST_DURATION:
                logger.info("Disregarding short period of fast speed %s",
                            fast_duration)
                continue

        # Get start and stop at 1Hz.
        slice_start_secs = slow_slice.start / speed.frequency
        slice_stop_secs = slow_slice.stop / speed.frequency

        slow_duration = slice_stop_secs - slice_start_secs
        if slow_duration < thresholds['min_split_duration']:
            logger.info("Disregarding period of speed below '%s' "
                        "since '%s' is shorter than MINIMUM_SPLIT_DURATION "
                        "('%s').", thresholds['speed_threshold'], slow_duration,
                        thresholds['min_split_duration'])
            continue

        last_fast_index = slow_slice.stop

        # Find split based on minimum of engine parameters.
        if split_params_min is not None:
            eng_split_index, eng_split_value = _split_on_eng_params(
                slice_start_secs, slice_stop_secs, split_params_min,
                split_params_frequency)
        else:
            eng_split_index, eng_split_value = None, None

        # Split using 'Frame Counter'.
        if dfc is not None:
            dfc_split_index = _split_on_dfc(
                slice_start_secs, slice_stop_secs, dfc.frequency,
                dfc_half_period, dfc_diff, eng_split_index=eng_split_index)
            if dfc_split_index:
                segments.append(_segment_type_and_slice(speed_array, speed.frequency,
                                                        heading.array, heading.frequency,
                                                        start, dfc_split_index, eng_arrays,
                                                        aircraft_info, thresholds, hdf))
                start = dfc_split_index
                logger.info("'Frame Counter' jumped within slow_slice '%s' "
                            "at index '%d'.", slow_slice, dfc_split_index)
                continue
            else:
                logger.info("'Frame Counter' did not jump within slow_slice "
                            "'%s'.", slow_slice)

        # Split using minimum of engine parameters.
        if eng_split_value is not None and \
           eng_split_value < settings.MINIMUM_SPLIT_PARAM_VALUE:
            logger.info("Minimum of normalised split parameters ('%s') was "
                        "below  ('%s') within "
                        "slow_slice '%s' at index '%d'.",
                        eng_split_value, settings.MINIMUM_SPLIT_PARAM_VALUE,
                        slow_slice, eng_split_index)
            segments.append(_segment_type_and_slice(speed_array, speed.frequency,
                                                    heading.array, heading.frequency,
                                                    start, eng_split_index, eng_arrays,
                                                    aircraft_info, thresholds, hdf))
            start = eng_split_index
            continue
        else:
            logger.info("Minimum of normalised split parameters ('%s') was "
                        "not below MINIMUM_SPLIT_PARAM_VALUE ('%s') within "
                        "slow_slice '%s' at index '%s'.",
                        eng_split_value, settings.MINIMUM_SPLIT_PARAM_VALUE,
                        slow_slice, eng_split_index)

        # Split using rate of turn. Q: Should this be considered in other
        # splitting methods.
        if rate_of_turn is None:
            continue

        rot_split_index = _split_on_rot(slice_start_secs, slice_stop_secs,
                                        heading.frequency, rate_of_turn)
        if rot_split_index:
            segments.append(_segment_type_and_slice(speed_array, speed.frequency,
                                                    heading.array, heading.frequency,
                                                    start, rot_split_index, eng_arrays,
                                                    aircraft_info, thresholds, hdf))
            start = rot_split_index
            logger.info("Splitting at index '%s' where rate of turn was below "
                        "'%s'.", rot_split_index,
                        settings.HEADING_RATE_SPLITTING_THRESHOLD)
            continue
        else:
            logger.info(
                "Aircraft did not stop turning during slow_slice "
                "('%s'). Therefore a split will not be made.", slow_slice)

        #Q: Raise error here?
        logger.warning("Splitting methods failed to split within slow_slice "
                       "'%s'.", slow_slice)

    # Add remaining data to a segment.
    segments.append(_segment_type_and_slice(speed_array, speed.frequency,
                                            heading.array, heading.frequency,
                                            start, speed_secs, eng_arrays,
                                            aircraft_info, thresholds, hdf))

    '''
    import matplotlib.pyplot as plt
    for look in [speed_array, heading.array, dfc.array, eng_arrays]:
        plt.plot(np.linspace(0, speed_secs, len(look)), look/np.ptp(look))
    for seg in segments:
        plt.plot([seg[1].start, seg[1].stop], [-0.5,+1])
    plt.show()
    '''

    return segments
def _segment_type_and_slice(speed_array, speed_frequency,
                            heading_array, heading_frequency,
                            start, stop, eng_arrays,
                            aircraft_info, thresholds, hdf):
    """
    Uses the Heading to determine whether the aircraft moved about at all and
    the airspeed to determine if it was a full or partial flight.

    NO_MOVEMENT: When the aircraft is in the hanger,
    the altitude and airspeed can be tested and record values which look like
    the aircraft is in flight; however the aircraft is rarely moved and the
    heading sensor is a good indication that this is a hanger test.

    GROUND_ONLY: If the heading changed, the airspeed needs to have been above
    the threshold speed for flight for a minimum amount of time, currently 3
    minutes to determine. If not, this segment is identified as GROUND_ONLY,
    probably taxiing, repositioning on the ground or a rejected takeoff.

    START_ONLY: If the airspeed started slow but ended fast, we had a partial
    segment for the start of a flight.

    STOP_ONLY:  If the airspeed started fast but ended slow, we had a partial
    segment for the end of a flight.

    MID_FLIGHT: The airspeed started and ended fast - no takeoff or landing!

    START_AND_STOP: The airspeed started and ended slow, implying a complete
    flight.

    segment_type is one of:
    * 'NO_MOVEMENT' (didn't change heading)
    * 'GROUND_ONLY' (didn't go fast)
    * 'START_AND_STOP'
    * 'START_ONLY'
    * 'STOP_ONLY'
    * 'MID_FLIGHT'
    """

    speed_start = start * speed_frequency
    speed_stop = stop * speed_frequency
    speed_array = speed_array[speed_start:speed_stop]

    heading_start = start * heading_frequency
    heading_stop = stop * heading_frequency
    heading_array = heading_array[heading_start:heading_stop]

    # remove small slices to find 'consistent' valid data
    unmasked_slices = slices_remove_small_slices(
        np.ma.clump_unmasked(speed_array), 10, speed_frequency)

    if unmasked_slices:
        # Check speed
        slow_start = speed_array[unmasked_slices[0].start] < thresholds['speed_threshold']
        slow_stop = speed_array[unmasked_slices[-1].stop - 1] < thresholds['speed_threshold']
        threshold_exceedance = np.ma.sum(
            speed_array > thresholds['speed_threshold']) / speed_frequency
        fast_for_long = threshold_exceedance > thresholds['min_duration']
    else:
        slow_start = slow_stop = fast_for_long = None

    # Find out if the aircraft moved
    if aircraft_info and aircraft_info['Aircraft Type'] == 'helicopter':
        try:
            gog = hdf['Gear On Ground']
        except:
            gog = hdf['Gear (R) On Ground']
        gog_start = start * gog.frequency
        gog_stop = stop * gog.frequency
        temp = np.ma.array(gog.array[gog_start:gog_stop].data, mask=gog.array[gog_start:gog_stop].mask)
        gog_test = np.ma.masked_less(temp, 1.0)
        # We have seeen 12-second spurious gog='Air' signals during rotor rundown. Hence increased limit.
        did_move = slices_remove_small_slices(np.ma.clump_masked(gog_test),
                                              time_limit=30, hz=gog.frequency)
    else:
        # Check Heading change for fixed wing.
        if eng_arrays is not None:
            heading_array = np.ma.masked_where(eng_arrays[heading_start:heading_stop] < settings.MIN_FAN_RUNNING, heading_array)
        hdiff = np.ma.abs(np.ma.diff(heading_array)).sum()
        did_move = hdiff > settings.HEADING_CHANGE_TAXI_THRESHOLD

    if not did_move or (not fast_for_long and eng_arrays is None):
        # added check for not fast for long and no engine params to avoid
        # lots of Herc ground runs
        logger.debug("Aircraft did not move.")
        segment_type = 'NO_MOVEMENT'
        # e.g. hanger tests, esp. if speed changes!
    elif not fast_for_long:
        logger.debug("speed was below threshold.")
        segment_type = 'GROUND_ONLY'  # e.g. RTO, re-positioning A/C
        #Q: report a go_fast?
    elif slow_start and slow_stop:
        logger.debug(
            "speed started below threshold, rose above and stopped below.")
        segment_type = 'START_AND_STOP'
    elif slow_start:
        logger.debug("speed started below threshold and stopped above.")
        segment_type = 'START_ONLY'
    elif slow_stop:
        logger.debug("speed started above threshold and stopped below.")
        segment_type = 'STOP_ONLY'
    else:
        logger.debug("speed started and stopped above threshold.")
        segment_type = 'MID_FLIGHT'
    logger.info("Segment type is '%s' between '%s' and '%s'.",
                segment_type, start, stop)
    return segment_type, slice(start, stop)
    def derive(self,
               gl=M('Gear (L) On Ground'),
               gr=M('Gear (R) On Ground'),
               vert_spd=P('Vertical Speed'),
               torque=P('Eng (*) Torque Avg'),
               ac_series=A('Series'),
               collective=P('Collective')):

        if gl and gr:
            delta = abs((gl.offset - gr.offset) * gl.frequency)
            if 0.75 < delta or delta < 0.25:
                # If the samples of the left and right gear are close together,
                # the best representation is to map them onto a single
                # parameter in which we accept that either wheel on the ground
                # equates to gear on ground.
                self.array = np.ma.logical_or(gl.array, gr.array)
                self.frequency = gl.frequency
                self.offset = gl.offset
                return
            else:
                # If the paramters are not co-located, then
                # merge_two_parameters creates the best combination possible.
                self.array, self.frequency, self.offset = merge_two_parameters(
                    gl, gr)
                return
        elif gl or gr:
            gear = gl or gr
            self.array = gear.array
            self.frequency = gear.frequency
            self.offset = gear.offset
        elif vert_spd and torque:
            vert_spd_limit = 100.0
            torque_limit = 30.0
            if ac_series and ac_series.value == 'Columbia 234':
                vert_spd_limit = 125.0
                torque_limit = 22.0
                collective_limit = 15.0

                vert_spd_array = align(
                    vert_spd,
                    torque) if vert_spd.hz != torque.hz else vert_spd.array
                collective_array = align(
                    collective,
                    torque) if collective.hz != torque.hz else collective.array

                vert_spd_array = moving_average(vert_spd_array)
                torque_array = moving_average(torque.array)
                collective_array = moving_average(collective_array)

                roo_vs_array = runs_of_ones(
                    abs(vert_spd_array) < vert_spd_limit, min_samples=1)
                roo_torque_array = runs_of_ones(torque_array < torque_limit,
                                                min_samples=1)
                roo_collective_array = runs_of_ones(
                    collective_array < collective_limit, min_samples=1)

                vs_and_torque = slices_and(roo_vs_array, roo_torque_array)
                grounded = slices_and(vs_and_torque, roo_collective_array)

                array = np_ma_zeros_like(vert_spd_array)
                for _slice in slices_remove_small_slices(grounded, count=2):
                    array[_slice] = 1
                array.mask = vert_spd_array.mask | torque_array.mask
                array.mask = array.mask | collective_array.mask
                self.array = nearest_neighbour_mask_repair(array)
                self.frequency = torque.frequency
                self.offset = torque.offset

            else:
                vert_spd_array = align(
                    vert_spd,
                    torque) if vert_spd.hz != torque.hz else vert_spd.array
                # Introducted for S76 and Bell 212 which do not have Gear On Ground available

                vert_spd_array = moving_average(vert_spd_array)
                torque_array = moving_average(torque.array)

                grounded = slices_and(
                    runs_of_ones(abs(vert_spd_array) < vert_spd_limit,
                                 min_samples=1),
                    runs_of_ones(torque_array < torque_limit, min_samples=1))

                array = np_ma_zeros_like(vert_spd_array)
                for _slice in slices_remove_small_slices(grounded, count=2):
                    array[_slice] = 1
                array.mask = vert_spd_array.mask | torque_array.mask
                self.array = nearest_neighbour_mask_repair(array)
                self.frequency = torque.frequency
                self.offset = torque.offset

        else:
            # should not get here if can_operate is correct
            raise NotImplementedError()
    def derive(self,
               gl=M('Gear (L) On Ground'),
               gr=M('Gear (R) On Ground'),
               vert_spd=P('Vertical Speed'),
               torque=P('Eng (*) Torque Avg'),
               ac_series=A('Series'),
               collective=P('Collective')):

        if gl and gr:
            delta = abs((gl.offset - gr.offset) * gl.frequency)
            if 0.75 < delta or delta < 0.25:
                # If the samples of the left and right gear are close together,
                # the best representation is to map them onto a single
                # parameter in which we accept that either wheel on the ground
                # equates to gear on ground.
                self.array = np.ma.logical_or(gl.array, gr.array)
                self.frequency = gl.frequency
                self.offset = gl.offset
                return
            else:
                # If the paramters are not co-located, then
                # merge_two_parameters creates the best combination possible.
                self.array, self.frequency, self.offset = merge_two_parameters(gl, gr)
                return
        elif gl or gr:
            gear = gl or gr
            self.array = gear.array
            self.frequency = gear.frequency
            self.offset = gear.offset
        elif vert_spd and torque:
            vert_spd_limit = 100.0
            torque_limit = 30.0
            if ac_series and ac_series.value == 'Columbia 234':
                vert_spd_limit = 125.0
                torque_limit = 22.0
                collective_limit = 15.0

                vert_spd_array = align(vert_spd, torque) if vert_spd.hz != torque.hz else vert_spd.array
                collective_array = align(collective, torque) if collective.hz != torque.hz else collective.array

                vert_spd_array = moving_average(vert_spd_array)
                torque_array = moving_average(torque.array)
                collective_array = moving_average(collective_array)

                roo_vs_array = runs_of_ones(abs(vert_spd_array) < vert_spd_limit, min_samples=1)
                roo_torque_array = runs_of_ones(torque_array < torque_limit, min_samples=1)
                roo_collective_array = runs_of_ones(collective_array < collective_limit, min_samples=1)

                vs_and_torque = slices_and(roo_vs_array, roo_torque_array)
                grounded = slices_and(vs_and_torque, roo_collective_array)

                array = np_ma_zeros_like(vert_spd_array)
                for _slice in slices_remove_small_slices(grounded, count=2):
                    array[_slice] = 1
                array.mask = vert_spd_array.mask | torque_array.mask
                array.mask = array.mask | collective_array.mask
                self.array = nearest_neighbour_mask_repair(array)
                self.frequency = torque.frequency
                self.offset = torque.offset

            else:
                vert_spd_array = align(vert_spd, torque) if vert_spd.hz != torque.hz else vert_spd.array
                # Introducted for S76 and Bell 212 which do not have Gear On Ground available

                vert_spd_array = moving_average(vert_spd_array)
                torque_array = moving_average(torque.array)

                grounded = slices_and(runs_of_ones(abs(vert_spd_array) < vert_spd_limit, min_samples=1),
                                      runs_of_ones(torque_array < torque_limit, min_samples=1))

                array = np_ma_zeros_like(vert_spd_array)
                for _slice in slices_remove_small_slices(grounded, count=2):
                    array[_slice] = 1
                array.mask = vert_spd_array.mask | torque_array.mask
                self.array = nearest_neighbour_mask_repair(array)
                self.frequency = torque.frequency
                self.offset = torque.offset

        else:
            # should not get here if can_operate is correct
            raise NotImplementedError()
    def derive(self,
               vert_spd=P('Vertical Speed'),
               torque=P('Eng (*) Torque Avg'),
               ac_series=A('Series'),
               collective=P('Collective')):

        vert_spd_limit = 100.0
        torque_limit = 30.0
        if ac_series and ac_series.value == 'Columbia 234':
            vert_spd_limit = 125.0
            torque_limit = 22.0
            collective_limit = 15.0

            vert_spd_array = align(
                vert_spd,
                torque) if vert_spd.hz != torque.hz else vert_spd.array
            collective_array = align(
                collective,
                torque) if collective.hz != torque.hz else collective.array

            vert_spd_array = moving_average(vert_spd_array)
            torque_array = moving_average(torque.array)
            collective_array = moving_average(collective_array)

            roo_vs_array = runs_of_ones(abs(vert_spd_array) < vert_spd_limit,
                                        min_samples=1)
            roo_torque_array = runs_of_ones(torque_array < torque_limit,
                                            min_samples=1)
            roo_collective_array = runs_of_ones(
                collective_array < collective_limit, min_samples=1)

            vs_and_torque = slices_and(roo_vs_array, roo_torque_array)
            grounded = slices_and(vs_and_torque, roo_collective_array)

            array = np_ma_zeros_like(vert_spd_array)
            for _slice in slices_remove_small_slices(grounded, count=2):
                array[_slice] = 1
            array.mask = vert_spd_array.mask | torque_array.mask
            array.mask = array.mask | collective_array.mask
            self.array = nearest_neighbour_mask_repair(array)
            self.frequency = torque.frequency
            self.offset = torque.offset

        else:
            vert_spd_array = align(
                vert_spd,
                torque) if vert_spd.hz != torque.hz else vert_spd.array
            # Introducted for S76 and Bell 212 which do not have Gear On Ground available

            vert_spd_array = moving_average(vert_spd_array)
            torque_array = moving_average(torque.array)

            grounded = slices_and(
                runs_of_ones(abs(vert_spd_array) < vert_spd_limit,
                             min_samples=1),
                runs_of_ones(torque_array < torque_limit, min_samples=1))

            array = np_ma_zeros_like(vert_spd_array)
            for _slice in slices_remove_small_slices(grounded, count=2):
                array[_slice] = 1
            array.mask = vert_spd_array.mask | torque_array.mask
            self.array = nearest_neighbour_mask_repair(array)
            self.frequency = torque.frequency
            self.offset = torque.offset