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 derive(self, alt_agl=P('Altitude AGL'), airs=S('Airborne'), gspd=P('Groundspeed'), trans_hfs=S('Transition Hover To Flight'), trans_fhs=S('Transition Flight To Hover')): low_flights = [] hovers = [] for air in airs: lows = slices_below(alt_agl.array[air.slice], HOVER_HEIGHT_LIMIT)[1] for low in lows: if np.ma.min(alt_agl.array[shift_slice( low, air.slice.start)]) <= HOVER_MIN_HEIGHT: low_flights.extend([shift_slice(low, air.slice.start)]) repaired_gspd = repair_mask(gspd.array, frequency=gspd.hz, repair_duration=8, method='fill_start') slows = slices_below(repaired_gspd, HOVER_GROUNDSPEED_LIMIT)[1] low_flights = slices_and(low_flights, slows) # Remove periods identified already as transitions. for low_flight in low_flights: if trans_fhs: for trans_fh in trans_fhs: if slices_overlap(low_flight, trans_fh.slice): low_flight = slice(trans_fh.slice.stop, low_flight.stop) if trans_hfs: for trans_hf in trans_hfs: if slices_overlap(low_flight, trans_hf.slice): low_flight = slice(low_flight.start, trans_hf.slice.start) hovers.extend([low_flight]) # Exclude transition periods and trivial periods of operation. self.create_phases( filter_slices_duration(hovers, HOVER_MIN_DURATION, frequency=alt_agl.frequency))
def derive(self, gnds=S('Grounded'), pitch=P('Pitch'), roll=P('Roll'), offshores=S('Offshore')): if offshores is not None: self.create_sections( slices_and(gnds.get_slices(edges=False), offshores.get_slices(edges=False))) return decks = [] for gnd in gnds: # The fourier transform for pitching motion... p = pitch.array[gnd.slice] if np.all(p.mask): continue n = float( len(p)) # Scaling the result to be independet of data length. fft_p = np.abs(np.fft.rfft(p - moving_average(p))) / n # similarly for roll r = roll.array[gnd.slice] if np.all(r.mask): continue fft_r = np.abs(np.fft.rfft(r - moving_average(r))) / n # What was the maximum harmonic seen? fft_max = np.ma.max(fft_p + fft_r) # Values of less than 0.1 were on the ground, and 0.34 on deck for the one case seen to date. if fft_max > 0.2: decks.append(gnd.slice) if decks: self.create_sections(decks)
def derive(self, alt_agl=P('Altitude AGL'), airs=S('Airborne'), gspd=P('Groundspeed'), trans_hfs=S('Transition Hover To Flight'), trans_fhs=S('Transition Flight To Hover')): low_flights = [] hovers = [] for air in airs: lows = slices_below(alt_agl.array[air.slice], HOVER_HEIGHT_LIMIT)[1] for low in lows: if np.ma.min(alt_agl.array[shift_slice(low, air.slice.start)]) <= HOVER_MIN_HEIGHT: low_flights.extend([shift_slice(low, air.slice.start)]) repaired_gspd = repair_mask(gspd.array, frequency=gspd.hz, repair_duration=8, method='fill_start') slows = slices_below(repaired_gspd, HOVER_GROUNDSPEED_LIMIT)[1] low_flights = slices_and(low_flights, slows) # Remove periods identified already as transitions. for low_flight in low_flights: if trans_fhs: for trans_fh in trans_fhs: if slices_overlap(low_flight, trans_fh.slice): low_flight = slice(trans_fh.slice.stop, low_flight.stop) if trans_hfs: for trans_hf in trans_hfs: if slices_overlap(low_flight, trans_hf.slice): low_flight = slice(low_flight.start, trans_hf.slice.start) hovers.extend([low_flight]) # Exclude transition periods and trivial periods of operation. self.create_phases(filter_slices_duration(hovers, HOVER_MIN_DURATION, frequency=alt_agl.frequency))
def derive(self, pitch=P('Pitch'), climbs=S('Initial Climb'), offshore=S('Offshore')): for climb_offshore in slices_and(climbs.get_slices(), offshore.get_slices()): climb_offshore = slices_int(climb_offshore) masked_pitch = mask_outside_slices(pitch.array, [climb_offshore]) pitch_index = np.ma.argmax( masked_pitch <= -10) or np.ma.argmin(masked_pitch) scaling_factor = abs(masked_pitch[pitch_index]) / 10 window_threshold = -10.00 * scaling_factor min_window_threshold = -8.00 * scaling_factor window_size = 32 window_threshold_step = 0.050 * scaling_factor diffs = np.ma.ediff1d( masked_pitch[climb_offshore.start:pitch_index]) diffs_exist = diffs.data.size >= 2 big_diff_index = -1 while diffs_exist: sig_pitch_threshold = window_threshold / window_size for i, d in enumerate(diffs): # Look for the first big negative pitch spike if diffs[slices_int( i, i + window_size)].sum() < window_threshold: # Find the first significant negative value within the # spike and make that the starting point of the phase big_diff_index = np.ma.argmax( diffs[i:i + window_size] < sig_pitch_threshold) + i break # Bail on match or total failure if big_diff_index != -1 or window_size < 2: break # Shrink window size instead of looking for insignificant # spikes and scale window/pitch thresholds accordingly if window_threshold >= min_window_threshold: window_size //= 2 min_window_threshold /= 2 window_threshold /= 2 window_threshold_step /= 2 sig_pitch_threshold *= 2 else: window_threshold += window_threshold_step if big_diff_index != -1: self.create_section( slice(climb_offshore.start + big_diff_index, pitch_index)) # Worst case fallback, this should happen extremely rarely # and would trigger all events related to this phase else: self.create_section( slice(climb_offshore.start, climb_offshore.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