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 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