def geo_locate(hdf, items): ''' Translate KeyTimeInstance into GeoKeyTimeInstance namedtuples ''' if 'Latitude Smoothed' not in hdf.valid_param_names() \ or 'Longitude Smoothed' not in hdf.valid_param_names(): logger.warning("Could not geo-locate as either 'Latitude Smoothed' or " "'Longitude Smoothed' were not found within the hdf.") return items lat_hdf = hdf['Latitude Smoothed'] lon_hdf = hdf['Longitude Smoothed'] if (not lat_hdf.array.count()) or (not lon_hdf.array.count()): logger.warning("Could not geo-locate as either 'Latitude Smoothed' or " "'Longitude Smoothed' have no unmasked values.") return items lat_pos = derived_param_from_hdf(lat_hdf) lon_pos = derived_param_from_hdf(lon_hdf) # We want to place start of flight and end of flight markers at the ends # of the data which may extend more than REPAIR_DURATION seconds beyond # the end of the valid data. Hence by setting this to None and # extrapolate=True we achieve this goal. lat_pos.array = repair_mask(lat_pos.array, repair_duration=None, extrapolate=True) lon_pos.array = repair_mask(lon_pos.array, repair_duration=None, extrapolate=True) for item in itertools.chain.from_iterable(six.itervalues(items)): item.latitude = lat_pos.at(item.index) or None item.longitude = lon_pos.at(item.index) or None return items
def derive(self, gl=M('Gear (L) Position'), gn=M('Gear (N) Position'), gr=M('Gear (R) Position'), gc=M('Gear (C) Position')): up_state = vstack_params_where_state( (gl, 'Up'), (gn, 'Up'), (gr, 'Up'), (gc, 'Up'), ).all(axis=0) down_state = vstack_params_where_state( (gl, 'Down'), (gn, 'Down'), (gr, 'Down'), (gc, 'Down'), ).all(axis=0) transit_state = vstack_params_where_state( (gl, 'In Transit'), (gn, 'In Transit'), (gr, 'In Transit'), (gc, 'In Transit'), ).any(axis=0) param = first_valid_parameter(gl, gn, gr, gc) self.array = np_ma_masked_zeros_like(param.array) self.array[repair_mask(up_state, repair_duration=None)] = 'Up' self.array[repair_mask(down_state, repair_duration=None)] = 'Down' self.array[repair_mask(transit_state, repair_duration=None)] = 'In Transit' self.array = nearest_neighbour_mask_repair(self.array)
def geo_locate(hdf, items): ''' Translate KeyTimeInstance into GeoKeyTimeInstance namedtuples ''' if 'Latitude Smoothed' not in hdf.valid_param_names() \ or 'Longitude Smoothed' not in hdf.valid_param_names(): logger.warning("Could not geo-locate as either 'Latitude Smoothed' or " "'Longitude Smoothed' were not found within the hdf.") return items lat_hdf = hdf['Latitude Smoothed'] lon_hdf = hdf['Longitude Smoothed'] if (not lat_hdf.array.count()) or (not lon_hdf.array.count()): logger.warning("Could not geo-locate as either 'Latitude Smoothed' or " "'Longitude Smoothed' have no unmasked values.") return items lat_pos = derived_param_from_hdf(lat_hdf) lon_pos = derived_param_from_hdf(lon_hdf) # We want to place start of flight and end of flight markers at the ends # of the data which may extend more than REPAIR_DURATION seconds beyond # the end of the valid data. Hence by setting this to None and # extrapolate=True we achieve this goal. lat_pos.array = repair_mask(lat_pos.array, repair_duration=None, extrapolate=True) lon_pos.array = repair_mask(lon_pos.array, repair_duration=None, extrapolate=True) for item in itertools.chain.from_iterable(items.itervalues()): item.latitude = lat_pos.at(item.index) or None item.longitude = lon_pos.at(item.index) or None return items
def derive(self, src_1=P('Latitude (1)'), src_2=P('Latitude (2)'), src_3=P('Latitude (3)')): sources = [ deepcopy(source) for source in [src_1, src_2, src_3] if source is not None \ and np.count_nonzero(np.ma.compressed(source.array)) > len(source.array)/2 ] for source in sources: source.array = repair_mask(source.array) if len(sources) == 1: self.offset = sources[0].offset self.frequency = sources[0].frequency self.array = sources[0].array elif len(sources) == 2: self.array, self.frequency, self.offset = blend_two_parameters( sources[0], sources[1]) elif len(sources) > 2: self.offset = 0.0 self.frequency = 1.0 self.array = blend_parameters(sources, offset=self.offset, frequency=self.frequency) else: self.array = np_ma_masked_zeros_like(src_1.array)
def derive(self, src_1=P('Longitude (1)'), src_2=P('Longitude (2)'), src_3=P('Longitude (3)')): sources = [ source for source in [src_1, src_2, src_3] if source is not None \ and np.count_nonzero(source.array) > len(source.array)/2 ] if len(sources) > 1: for source in sources: source.array = repair_mask( straighten_longitude(source.array) + 180.0 ) if len(sources) == 1: self.offset = sources[0].offset self.frequency = sources[0].frequency self.array = sources[0].array elif len(sources) == 2: blended, self.frequency, self.offset = blend_two_parameters( sources[0], sources[1] ) self.array = blended % 360 - 180.0 elif len(sources) > 2: self.offset = 0.0 self.frequency = 1.0 blended = blend_parameters(sources, offset=self.offset, frequency=self.frequency) self.array = blended % 360 - 180.0
def derive(self, src_1=P('Longitude (1)'), src_2=P('Longitude (2)'), src_3=P('Longitude (3)')): sources = [ source for source in [src_1, src_2, src_3] if source is not None \ and np.count_nonzero(source.array) > len(source.array)/2 ] if len(sources) > 1: for source in sources: source.array = repair_mask( straighten_longitude(source.array) + 180.0) if len(sources) == 1: self.offset = sources[0].offset self.frequency = sources[0].frequency self.array = sources[0].array elif len(sources) == 2: blended, self.frequency, self.offset = blend_two_parameters( sources[0], sources[1]) self.array = blended % 360 - 180.0 elif len(sources) > 2: self.offset = 0.0 self.frequency = 1.0 blended = blend_parameters(sources, offset=self.offset, frequency=self.frequency) self.array = blended % 360 - 180.0 else: self.array = np_ma_masked_zeros_like(src_1.array)
def geo_locate(hdf, items): ''' Translate KeyTimeInstance into GeoKeyTimeInstance namedtuples ''' if 'Latitude Smoothed' not in hdf.valid_param_names() \ or 'Longitude Smoothed' not in hdf.valid_param_names(): logger.warning("Could not geo-locate as either 'Latitude Smoothed' or " "'Longitude Smoothed' were not found within the hdf.") return items lat_pos = derived_param_from_hdf(hdf['Latitude Smoothed']) lon_pos = derived_param_from_hdf(hdf['Longitude Smoothed']) lat_rep = repair_mask(lat_pos, extrapolate=True) lon_rep = repair_mask(lon_pos, extrapolate=True) for item in items: item.latitude = lat_rep.at(item.index) or None item.longitude = lon_rep.at(item.index) or None return items
def geo_locate(hdf, items): ''' Translate KeyTimeInstance into GeoKeyTimeInstance namedtuples ''' if 'Latitude Smoothed' not in hdf.valid_param_names() \ or 'Longitude Smoothed' not in hdf.valid_param_names(): logger.warning("Could not geo-locate as either 'Latitude Smoothed' or " "'Longitude Smoothed' were not found within the hdf.") return items lat_pos = derived_param_from_hdf(hdf['Latitude Smoothed']) lon_pos = derived_param_from_hdf(hdf['Longitude Smoothed']) lat_pos.array = repair_mask(lat_pos.array, extrapolate=True) lon_pos.array = repair_mask(lon_pos.array, extrapolate=True) for item in items: item.latitude = lat_pos.at(item.index) or None item.longitude = lon_pos.at(item.index) or None return items
def derive(self, airspeed=P('Airspeed True'), grounded=S('Grounded')): for section in grounded: # zero out travel on the ground airspeed.array[section.slice] = 0.0 # this is already a copy repaired_array = repair_mask( airspeed.array) # to avoid integration hiccups adist = integrate(repaired_array, airspeed.frequency, scale=1.0 / 3600.0) self.array = adist
def _rate_of_turn(heading): ''' Create rate of turn from heading. :param heading: Heading parameter. :type heading: Parameter ''' heading.array = repair_mask(straighten_headings(heading.array), repair_duration=None) rate_of_turn = np.ma.abs(rate_of_change(heading, 8)) rate_of_turn_masked = \ np.ma.masked_greater(rate_of_turn, settings.HEADING_RATE_SPLITTING_THRESHOLD) return rate_of_turn_masked
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, 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, airspeed=P('Airspeed True'), grounded=S('Grounded') ): for section in grounded: # zero out travel on the ground airspeed.array[section.slice]=0.0 # this is already a copy repaired_array = repair_mask(airspeed.array) # to avoid integration hiccups adist = integrate( repaired_array, airspeed.frequency, scale=1.0/3600.0 ) self.array = adist
def derive( self, alt_aal=P('Altitude AAL'), alt_agl=P('Altitude AGL'), ac_type=A('Aircraft Type'), app=S('Approach And Landing'), hdg=P('Heading Continuous'), lat=P('Latitude Prepared'), lon=P('Longitude Prepared'), ils_loc=P('ILS Localizer'), ils_gs=S('ILS Glideslope'), ils_freq=P('ILS Frequency'), land_afr_apt=A('AFR Landing Airport'), land_afr_rwy=A('AFR Landing Runway'), lat_land=KPV('Latitude At Touchdown'), lon_land=KPV('Longitude At Touchdown'), precision=A('Precise Positioning'), fast=S('Fast'), #lat_smoothed=P('Latitude Smoothed'), #lon_smoothed=P('Longitude Smoothed'), u=P('Airspeed'), gspd=P('Groundspeed'), height_from_rig=P('Altitude ADH'), hdot=P('Vertical Speed'), roll=P('Roll'), heading=P('Heading'), distance_land=P('Distance To Landing'), tdwns=KTI('Touchdown'), offshore=M('Offshore'), takeoff=S('Takeoff')): precise = bool(getattr(precision, 'value', False)) alt = alt_agl if ac_type == helicopter else alt_aal app_slices = sorted(app.get_slices()) for index, _slice in enumerate(app_slices): # a) The last approach is assumed to be landing: if index == len(app_slices) - 1: approach_type = 'LANDING' landing = True # b) We have a touch and go if Altitude AAL reached zero: #elif np.ma.any(alt.array[_slice] <= 0): elif np.ma.any(alt.array[_slice.start:_slice.stop + (5 * alt.frequency)] <= 0): if ac_type == aeroplane: approach_type = 'TOUCH_AND_GO' landing = False elif ac_type == helicopter: approach_type = 'LANDING' landing = True else: raise ValueError('Not doing hovercraft!') # c) In any other case we have a go-around: else: approach_type = 'GO_AROUND' landing = False # Rough reference index to allow for go-arounds ref_idx = index_at_value(alt.array, 0.0, _slice=_slice, endpoint='nearest') turnoff = None if landing: search_end = fast.get_surrounding(_slice.start) if search_end and search_end[0].slice.stop >= ref_idx: search_end = min(search_end[0].slice.stop, _slice.stop) else: search_end = _slice.stop tdn_hdg = np.ma.median(hdg.array[ref_idx:search_end + 1]) # Complex trap for the all landing heading data is masked case... if (tdn_hdg % 360.0) is np.ma.masked: lowest_hdg = bearing_and_distance(lat.array[ref_idx], lon.array[ref_idx], lat.array[search_end], lon.array[search_end])[0] else: lowest_hdg = (tdn_hdg % 360.0).item() # While we're here, let's compute the turnoff index for this landing. head_landing = hdg.array[(ref_idx + _slice.stop) / 2:_slice.stop] if len(head_landing) > 2: peak_bend = peak_curvature(head_landing, curve_sense='Bipolar') fifteen_deg = index_at_value( np.ma.abs(head_landing - head_landing[0]), 15.0) if peak_bend: turnoff = (ref_idx + _slice.stop) / 2 + peak_bend else: if fifteen_deg and fifteen_deg < peak_bend: turnoff = start_search + landing_turn else: # No turn, so just use end of landing run. turnoff = _slice.stop else: # No turn, so just use end of landing run. turnoff = _slice.stop else: # We didn't land, but this is indicative of the runway heading lowest_hdg = (hdg.array[ref_idx] % 360.0).item() # Pass latitude, longitude and heading lowest_lat = None lowest_lon = None if lat and lon and ref_idx: lowest_lat = lat.array[ref_idx] or None lowest_lon = lon.array[ref_idx] or None if lowest_lat and lowest_lon and approach_type == 'GO_AROUND': # Doing a go-around, we extrapolate to the threshold # in case we abort the approach abeam a different airport, # using the rule of three miles per thousand feet. distance = np.ma.array([ ut.convert(alt_aal.array[ref_idx] * (3 / 1000.0), ut.NM, ut.METER) ]) bearing = np.ma.array([lowest_hdg]) reference = { 'latitude': lowest_lat, 'longitude': lowest_lon } lat_ga, lon_ga = latitudes_and_longitudes( bearing, distance, reference) lowest_lat = lat_ga[0] lowest_lon = lon_ga[0] if lat_land and lon_land and not (lowest_lat and lowest_lon): # use lat/lon at landing if values at ref_idx are masked # only interested in landing within approach slice. lat_land = lat_land.get(within_slice=_slice) lon_land = lon_land.get(within_slice=_slice) if lat_land and lon_land: lowest_lat = lat_land[0].value or None lowest_lon = lon_land[0].value or None kwargs = dict( precise=precise, _slice=_slice, lowest_lat=lowest_lat, lowest_lon=lowest_lon, lowest_hdg=lowest_hdg, appr_ils_freq=None, ac_type=ac_type, ) # If the approach is a landing, pass through information from the # achieved flight record in case we cannot determine airport and # runway: if landing: kwargs.update( land_afr_apt=land_afr_apt, land_afr_rwy=land_afr_rwy, hint='landing', ) if landing or approach_type == 'GO_AROUND': # if we have a frequency and valid localiser signal at lowest point in approach appr_ils_freq = None if ils_freq: appr_ils_freq = np.ma.round(ils_freq.array[ref_idx] or 0, 2) if not precise and appr_ils_freq and ils_loc and np.ma.abs( ils_loc.array[ref_idx]) < 2.5: kwargs['appr_ils_freq'] = appr_ils_freq airport, landing_runway = self._lookup_airport_and_runway(**kwargs) if not airport and ac_type == aeroplane: continue if ac_type == aeroplane and not airport.get('runways'): self.error("Airport %s: contains no runways", airport['code']) # Simple determination of heliport. heliport = is_heliport(ac_type, airport, landing_runway) sorted_tdwns = sorted(tdwns, key=lambda touchdown: touchdown.index) sorted_takeoffs = sorted(takeoff.get_slices(), key=lambda tkoff: tkoff.start) for touchdown, tkoff in zip(sorted_tdwns, sorted_takeoffs): # If both the takeoff and touchdown point are offshore then we consider # the approach to be a 'SHUTTLING APPROACH'. Else we continue to look for # an 'AIRBORNE RADAR DIRECT/OVERHEAD APPROACH' or a 'STANDARD APPROACH' # # A couple of seconds are added to the end of the slice as some flights used # to test this had the touchdown a couple of seconds outside the approach slice if is_index_within_slice( touchdown.index, slice(_slice.start, _slice.stop + 5 * alt.frequency)): if offshore and offshore.array[ touchdown. index] == 'Offshore' and tkoff.start < touchdown.index: if not distance_land: if offshore.array[tkoff.start] == 'Offshore': approach_type = 'SHUTTLING' elif offshore.array[ tkoff. start] == 'Offshore' and tkoff.start < len( distance_land.array ) and distance_land.array[tkoff.start] <= 40: approach_type = 'SHUTTLING' elif height_from_rig: Vy = 80.0 # Type dependent? # conditions_defs is a dict of condition name : expression to evaluate pairs, listed this way for clarity condition_defs = { 'Below 120 kts': lambda p: p['Airspeed'] < 120, 'Below Vy+5': lambda p: p['Airspeed'] < Vy + 5.0, 'Over Vy': lambda p: p['Airspeed'] > Vy, 'Over Vy-5': lambda p: p['Airspeed'] > Vy - 5.0, 'Below 70 gspd': lambda p: p['Groundspeed'] < 72, 'Below 60 gspd': lambda p: p['Groundspeed'] < 60, #'Below Vy-10' : lambda p : p['Airspeed'] < Vy-10.0, #'Over Vy-10' : lambda p : p['Airspeed'] > Vy-10.0, #'Above 30 gspd' : lambda p : p['Groundspeed'] > 30, 'Over 900 ft': lambda p: p['Altitude ADH'] > 900, 'Over 200 ft': lambda p: p['Altitude ADH'] > 200, 'Below 1750 ft': lambda p: p['Altitude ADH'] < 1750, 'Below 1100 ft': lambda p: p['Altitude ADH'] < 1100, 'Over 350 ft': lambda p: p['Altitude ADH'] > 350, 'Below 700 ft': lambda p: p['Altitude ADH'] < 700, 'ROD < 700 fpm': lambda p: p['Vertical Speed'] > -700, 'ROD > 200 fpm': lambda p: p['Vertical Speed'] < -200, 'Not climbing': lambda p: p['Vertical Speed'] < 200, #'Over 400 ft' : lambda p : p['Altitude ADH'] > 400, #'Below 1500 ft': lambda p : p['Altitude ADH'] < 1500, #'Below 1300 ft': lambda p : p['Altitude ADH'] < 1300, 'Roll below 25 deg': lambda p: valid_between( p['Roll'], -25.0, 25.0), 'Wings Level': lambda p: valid_between( p['Roll'], -10.0, 10.0), 'Within 20 deg of final heading': lambda p: np.ma.abs(p['head_off_final']) < 20.0, #'Within 45 deg of downwind leg' : 'valid_between(np.ma.abs(head_off_final), 135.0, 225.0)', #'15 deg off final heading' : lambda p : np.ma.abs(np.ma.abs(p['head_off_two_miles'])-15.0) < 5.0, #'Heading towards oil rig' : lambda p : np.ma.abs(p['head_off_two_miles']) < 6.0, 'Beyond 0.7 NM': lambda p: p['Distance To Landing'] > 0.7, 'Within 0.8 NM': lambda p: p['Distance To Landing'] < 0.8, 'Beyond 1.5 NM': lambda p: p['Distance To Landing'] > 1.5, 'Within 2.0 NM': lambda p: p['Distance To Landing'] < 2.0, 'Within 3.0 NM': lambda p: p['Distance To Landing'] < 3.0, 'Beyond 3.0 NM': lambda p: p['Distance To Landing'] > 3.0, 'Within 10.0 NM': lambda p: p['Distance To Landing'] < 10.0, #'Within 1.5 NM' : lambda p : p['Distance To Landing'] < 1.5, } # Phase map is a dict of the flight phases with the list of conditions which must be # satisfied for the phase to be active. phase_map = { 'Circuit': [ 'Below 120 kts', 'Over Vy', 'Below 1100 ft', 'Over 900 ft', 'Roll below 25 deg', # includes downwind turn ], 'Level within 2NM': [ 'Below Vy+5', 'Over Vy-5', 'Below 1100 ft', 'Over 900 ft', 'Wings Level', 'Within 20 deg of final heading', 'Within 2.0 NM', 'Beyond 1.5 NM', ], 'Initial Descent': [ 'Wings Level', 'Within 20 deg of final heading', 'ROD < 700 fpm', 'ROD > 200 fpm', 'Beyond 0.7 NM', 'Over 350 ft', ], 'Final Approach': [ 'Wings Level', 'Within 20 deg of final heading', 'ROD < 700 fpm', 'Within 0.8 NM', 'Below 60 gspd', 'Below 700 ft', ], # Phases for ARDA/AROA # # All heading conditions are commented out as the pilots usually # go outside the boundaries; the other conditions seem to be # enough to detect them 'ARDA/AROA 10 to 3': [ 'Within 10.0 NM', 'Beyond 3.0 NM', 'Below 1750 ft', 'Not climbing', #'Heading towards oil rig', ], 'ARDA/AROA Level within 3NM': [ 'Below 70 gspd', 'Over 200 ft', 'Wings Level', 'Within 3.0 NM', 'Beyond 1.5 NM', #'Within 20 deg of final heading', ], 'ARDA/AROA Final': [ 'Not climbing', 'Within 2.0 NM', #'15 deg off final heading' ], } """ #Phases that can be used to tighten the conditions for ARDA/AROA 'Radar Heading Change':['15 deg off final heading', 'Within 1.5 NM', 'Beyond 0.7 NM'], 'Low Approach':['Below Vy+5', 'Below 700 ft', 'Over 350 ft', 'Within 20 deg of final heading', 'Wings Level'], 'Low Circuit':['Below 120 kts', 'Over Vy-5', 'Below 700 ft', 'Over 350 ft', 'Roll below 25 deg'] """ approach_map = { 'RIG': [ 'Circuit', 'Level within 2NM', 'Initial Descent', 'Final Approach' ], 'AIRBORNE_RADAR': [ 'ARDA/AROA 10 to 3', 'ARDA/AROA Level within 3NM', 'ARDA/AROA Final' ] } # Making sure the approach slice contains enough information to be able # to properly identify ARDA/AROA approaches (the procedure starts from 10NM # before touchdown) app_slice = slice( index_at_value(distance_land.array, 11, _slice=slice( 0, touchdown.index)), touchdown.index) heading_repaired = repair_mask( heading.array[app_slice], frequency=heading.frequency, repair_duration=np.ma.count_masked( heading.array[app_slice]), copy=True, extrapolate=True) param_arrays = { 'Airspeed': u.array[app_slice], 'Groundspeed': gspd.array[app_slice], 'Altitude ADH': height_from_rig.array[app_slice], 'Vertical Speed': hdot.array[app_slice], 'Roll': roll.array[app_slice], 'Distance To Landing': distance_land.array[app_slice], 'Heading': heading_repaired, 'Latitude': lat.array[app_slice], 'Longitude': lon.array[app_slice], } longest_approach_type, longest_approach_durn, longest_approach_slice = find_rig_approach( condition_defs, phase_map, approach_map, Vy, None, param_arrays, debug=False) if longest_approach_type is not None: approach_type = longest_approach_type.upper() _slice = slice( app_slice.start + longest_approach_slice.start, app_slice.stop) if heliport: self.create_approach( approach_type, _slice, runway_change=False, offset_ils=False, airport=airport, landing_runway=None, approach_runway=None, gs_est=None, loc_est=None, ils_freq=None, turnoff=None, lowest_lat=lowest_lat, lowest_lon=lowest_lon, lowest_hdg=lowest_hdg, ) continue ######################################################################### ## Analysis of fixed wing approach to a runway ## ## First step is to check the ILS frequency for the runway in use ## and cater for a change from the approach runway to the landing runway. ######################################################################### appr_ils_freq = None runway_change = False offset_ils = False # Do we have a recorded ILS frequency? If so, what was it tuned to at the start of the approach?? if ils_freq: appr_ils_freq = ils_freq.array[_slice.start] # Was this valid, and if so did the start of the approach match the landing runway? if appr_ils_freq and not (np.isnan(appr_ils_freq) or np.ma.is_masked(appr_ils_freq)): appr_ils_freq = round(appr_ils_freq, 2) runway_kwargs = { 'ilsfreq': appr_ils_freq, 'latitude': lowest_lat, 'longitude': lowest_lon, } if not precise: runway_kwargs['hint'] = kwargs.get('hint', 'approach') approach_runway = nearest_runway(airport, lowest_hdg, **runway_kwargs) # Have we have identified runways for both conditions that are both different and parallel? if all((approach_runway, landing_runway)) \ and approach_runway['id'] != landing_runway['id'] \ and approach_runway['identifier'][:2] == landing_runway['identifier'][:2]: runway_change = True else: # Without a frequency source, we just have to hope any localizer signal is for this runway! approach_runway = landing_runway if approach_runway and 'frequency' in approach_runway['localizer']: if np.ma.count(ils_loc.array[_slice]) > 10: if runway_change: # We only use the first frequency tuned. This stops scanning across both runways if the pilot retunes. loc_slice = shift_slices( runs_of_ones( np.ma.abs(ils_freq.array[_slice] - appr_ils_freq) < 0.001), _slice.start)[0] else: loc_slice = _slice else: # No localizer or inadequate data for this approach. loc_slice = None else: # The approach was to a runway without an ILS, so even if it was tuned, we ignore this. appr_ils_freq = None loc_slice = None if np.ma.is_masked(appr_ils_freq): loc_slice = None appr_ils_freq = None else: if appr_ils_freq and loc_slice: if appr_ils_freq != round( ut.convert( approach_runway['localizer']['frequency'], ut.KHZ, ut.MHZ), 2): loc_slice = None ####################################################################### ## Identification of the period established on the localizer ####################################################################### loc_est = None if loc_slice: valid_range = np.ma.flatnotmasked_edges(ils_loc.array[_slice]) # I have some data to scan. Shorthand names; loc_start = valid_range[0] + _slice.start loc_end = valid_range[1] + _slice.start scan_back = slice(ref_idx, loc_start, -1) # If we are turning in, we are not interested in signals that are not related to this approach. # The value of 45 deg was selected to encompass Washington National airport with a 40 deg offset. hdg_diff = np.ma.abs( np.ma.mod((hdg.array - lowest_hdg) + 180.0, 360.0) - 180.0) ils_hdg_45 = index_at_value(hdg_diff, 45.0, _slice=scan_back) # We are not interested above 1,500 ft, so may trim back the start point to that point: ils_alt_1500 = index_at_value(alt_aal.array, 1500.0, _slice=scan_back) # The criteria for start of established phase is the latter of the approach phase start, the turn-in or 1500ft. # The "or 0" allow for flights that do not turn through 45 deg or keep below 1500ft. loc_start = max(loc_start, ils_hdg_45 or 0, ils_alt_1500 or 0) if loc_start < ref_idx: # Did I get established on the localizer, and if so, # when? We only look AFTER the aircraft is already within # 45deg of the runway heading, below 1500ft and the data # is valid for this runway. Testing that the aircraft is # not just passing across the localizer is built into the # ils_established function. loc_estab = ils_established(ils_loc.array, slice(loc_start, ref_idx), ils_loc.hz) else: # If localiser start is after we touchdown bail. loc_estab = None if loc_estab: # Refine the end of the localizer established phase... if (approach_runway and approach_runway['localizer']['is_offset']): offset_ils = True # The ILS established phase ends when the deviation becomes large. loc_end = ils_established(ils_loc.array, slice( ref_idx, loc_estab, -1), ils_loc.hz, point='immediate') elif approach_type in ['TOUCH_AND_GO', 'GO_AROUND']: # We finish at the lowest point loc_end = ref_idx elif runway_change: # Use the end of localizer phase as this already reflects the tuned frequency. est_end = ils_established(ils_loc.array, slice(loc_estab, ref_idx), ils_loc.hz, point='end') # Make sure we dont end up with a negative slice i.e. end before we are established. loc_end = min([ x for x in (loc_slice.stop, loc_end, est_end or np.inf) if x > loc_estab ]) elif approach_type == 'LANDING': # Just end at 2 dots where we turn off the runway loc_end_2_dots = index_at_value( np.ma.abs(ils_loc.array), 2.0, _slice=slice( turnoff + 5 * (_slice.stop - _slice.start) / 100, loc_estab, -1)) if loc_end_2_dots and is_index_within_slice( loc_end_2_dots, _slice) and not np.ma.is_masked( ils_loc.array[loc_end_2_dots] ) and loc_end_2_dots > loc_estab: loc_end = loc_end_2_dots loc_est = slice(loc_estab, loc_end + 1) ####################################################################### ## Identification of the period established on the glideslope ####################################################################### gs_est = None if loc_est and 'glideslope' in approach_runway and ils_gs: # We only look for glideslope established periods if the localizer is already established. # The range to scan for the glideslope starts with localizer capture and ends at # 200ft or the minimum height point for a go-around or the end of # localizer established, if either is earlier. ils_gs_start = loc_estab ils_gs_200 = index_at_value(alt.array, 200.0, _slice=slice( loc_end, ils_gs_start, -1)) # The expression "ils_gs_200 or np.inf" caters for the case where the aircraft did not pass # through 200ft, so the result is None, in which case any other value is left to be the minimum. ils_gs_end = min(ils_gs_200 or np.inf, ref_idx, loc_end) # Look for ten seconds within half a dot ils_gs_estab = ils_established(ils_gs.array, slice(ils_gs_start, ils_gs_end), ils_gs.hz) if ils_gs_estab: gs_est = slice(ils_gs_estab, ils_gs_end + 1) ''' # These statements help set up test cases. print() print(airport['name']) print(approach_runway['identifier']) print(landing_runway['identifier']) print(_slice) if loc_est: print('Localizer established ', loc_est.start, loc_est.stop) if gs_est: print('Glideslope established ', gs_est.start, gs_est.stop) print() ''' self.create_approach( approach_type, _slice, runway_change=runway_change, offset_ils=offset_ils, airport=airport, landing_runway=landing_runway, approach_runway=approach_runway, gs_est=gs_est, loc_est=loc_est, ils_freq=appr_ils_freq, turnoff=turnoff, lowest_lat=lowest_lat, lowest_lon=lowest_lon, lowest_hdg=lowest_hdg, )
def derive( self, air_spd=P('Airspeed'), flap=P('Flap'), #conf=P('Configuration'), gw=P('Gross Weight Smoothed'), touchdowns=KTI('Touchdown'), series=A('Series'), family=A('Family'), engine=A('Engine Series'), engine_type=A('Engine Type'), eng_np=P('Eng (*) Np Avg'), vref=P('Vref'), afr_vref=A('AFR Vref'), approaches=S('Approach And Landing')): if vref: # Use recorded Vref parameter: self.array = vref.array elif afr_vref: # Use provided Vref from achieved flight record: afr_vspeed = afr_vref self.array = np.ma.zeros(len(air_spd.array), np.double) self.array.mask = True for approach in approaches: self.array[approach.slice] = afr_vspeed.value else: # Use speed card lookups x = map(lambda x: x.value if x else None, (series, family, engine, engine_type)) vspeed_class_test = get_vspeed_map_mitre(*x) if vspeed_class_test: vspeed_class = vspeed_class_test else: vspeed_class = get_vspeed_map(*x) if gw is not None: # and you must have eng_np try: # Allow up to 2 superframe values to be repaired: # (64 * 2 = 128 + a bit) repaired_gw = repair_mask(gw.array, repair_duration=130, copy=True, extrapolate=True) except: self.warning( "'Airspeed Reference' will be fully masked " "because 'Gross Weight Smoothed' array could not be " "repaired.") return setting_param = flap #or conf vspeed_table = vspeed_class() for approach in approaches: _slice = approach.slice '''TODO: Only uses max Vref setting, doesn't account for late config changes''' index = np.ma.argmax(setting_param.array[_slice]) setting = setting_param.array[_slice][index] weight = repaired_gw[_slice][ index] if gw is not None else None if setting in vspeed_table.vref_settings: ##and is_index_within_slice(touchdowns.get_last().index, _slice): # setting in vspeed table: vspeed = vspeed_table.vref(setting, weight) else: ''' Do not like the default of using max Vref for go arounds... ''' ## No landing and max setting not in vspeed table: #if setting_param.name == 'Flap': #setting = max(get_flap_map(series.value, family.value)) #else: #setting = max(get_conf_map(series.value, family.value).keys()) #vspeed = vspeed_table.vref(setting, weight) self.warning( "'Airspeed Reference' will be fully masked " "because Vref lookup table does not have corresponding values." ) return self.array = np_ma_masked_zeros_like(air_spd.array) self.array[_slice] = vspeed
def derive(self, nr=P('Nr')): self.array = np.ma.where( repair_mask(nr.array) > ROTORS_TURNING, 'Running', 'Not Running')
def derive(self, air_spd=P('Airspeed'), flap=P('Flap'), #conf=P('Configuration'), gw=P('Gross Weight Smoothed'), touchdowns=KTI('Touchdown'), series=A('Series'), family=A('Family'), engine=A('Engine Series'), engine_type=A('Engine Type'), eng_np=P('Eng (*) Np Avg'), vref=P('Vref'), afr_vref=A('AFR Vref'), approaches=S('Approach And Landing')): if vref: # Use recorded Vref parameter: self.array=vref.array elif afr_vref: # Use provided Vref from achieved flight record: afr_vspeed = afr_vref self.array = np.ma.zeros(len(air_spd.array), np.double) self.array.mask = True for approach in approaches: self.array[approach.slice] = afr_vspeed.value else: # Use speed card lookups x = map(lambda x: x.value if x else None, (series, family, engine, engine_type)) vspeed_class_test = get_vspeed_map_mitre(*x) if vspeed_class_test: vspeed_class = vspeed_class_test else: vspeed_class = get_vspeed_map(*x) if gw is not None: # and you must have eng_np try: # Allow up to 2 superframe values to be repaired: # (64 * 2 = 128 + a bit) repaired_gw = repair_mask(gw.array, repair_duration=130, copy=True, extrapolate=True) except: self.warning("'Airspeed Reference' will be fully masked " "because 'Gross Weight Smoothed' array could not be " "repaired.") return setting_param = flap #or conf vspeed_table = vspeed_class() for approach in approaches: _slice = approach.slice '''TODO: Only uses max Vref setting, doesn't account for late config changes''' index = np.ma.argmax(setting_param.array[_slice]) setting = setting_param.array[_slice][index] weight = repaired_gw[_slice][index] if gw is not None else None if setting in vspeed_table.vref_settings: ##and is_index_within_slice(touchdowns.get_last().index, _slice): # setting in vspeed table: vspeed = vspeed_table.vref(setting, weight) else: ''' Do not like the default of using max Vref for go arounds... ''' ## No landing and max setting not in vspeed table: #if setting_param.name == 'Flap': #setting = max(get_flap_map(series.value, family.value)) #else: #setting = max(get_conf_map(series.value, family.value).keys()) #vspeed = vspeed_table.vref(setting, weight) self.warning("'Airspeed Reference' will be fully masked " "because Vref lookup table does not have corresponding values.") return self.array = np_ma_masked_zeros_like(air_spd.array) self.array[_slice] = vspeed
def derive(self, nr=P('Nr')): self.array = np.ma.where(repair_mask(nr.array) > ROTORS_TURNING, 'Running', 'Not Running')
def track_to_kml(hdf_path, kti_list, kpv_list, approach_list, plot_altitude=None, dest_path=None): ''' Plot results of process_flight onto a KML track. :param flight_attrs: List of Flight Attributes :type flight_attrs: list :param plot_altitude: Name of Altitude parameter to use in KML :type plot_altitude: String ''' one_hz = Parameter() kml = simplekml.Kml() with hdf_file(hdf_path) as hdf: # Latitude param, Longitude param, track name, colour coord_params = ( {'lat': 'Latitude Smoothed', 'lon': 'Longitude Smoothed', 'track': 'Smoothed', 'colour': 'ff7fff7f'}, {'lat': 'Latitude Prepared', 'lon': 'Longitude Prepared', 'track': 'Prepared', 'colour': 'ffA11EB3'}, {'lat': 'Latitude', 'lon': 'Longitude', 'track': 'Recorded', 'colour': 'ff0000ff'}, {'lat': 'Latitude (Coarse)', 'lon': 'Longitude (Coarse)', 'track': 'Coarse', 'colour': 'ff0000ff'}, ) altitude_absolute_params = ('Altitude Visualization With Ground Offset', 'Altitude QNH', 'Altitude STD', 'Altitude AAL') altitude_relative_params = ('Altitude Radio',) # Check latitude and longitude pair exist. if not any(c['lat'] in hdf and c['lon'] in hdf for c in coord_params): logger.error("Cannot write track as coordinate paarmeters not in hdf") return False # Choose best altitude parameter if not specified. if not plot_altitude: altitude_params = itertools.chain(altitude_absolute_params, altitude_relative_params) try: plot_altitude = next(p for p in altitude_params if p in hdf) except StopIteration: logger.warning("Disabling altitude on KML plot as it is " "unavailable.") # Get altitude param from hdf. if plot_altitude and plot_altitude in hdf.keys(): alt = derived_param_from_hdf(hdf[plot_altitude]).get_aligned(one_hz) alt.array = repair_mask(alt.array, frequency=alt.frequency, repair_duration=None) alt.array = ut.convert(alt.array, ut.FT, ut.METER) else: alt = None if plot_altitude in altitude_absolute_params: altitude_mode = simplekml.constants.AltitudeMode.absolute elif plot_altitude in altitude_relative_params: altitude_mode = simplekml.constants.AltitudeMode.relativetoground else: altitude_mode = simplekml.constants.AltitudeMode.clamptoground ## Get best latitude and longitude parameters. best_lat = None best_lon = None for coord_config in coord_params: lat_name = coord_config['lat'] lon_name = coord_config['lon'] if not lat_name in hdf or not lon_name in hdf: continue lat = hdf[lat_name] lon = hdf[lon_name] best = not best_lat or not best_lon add_track(kml, coord_config['track'], lat, lon, coord_config['colour'], alt_param=alt, alt_mode=altitude_mode, visible=best) add_track(kml, coord_config['track'] + ' On Ground', lat, lon, coord_config['colour'], visible=best) if best: best_lat = derived_param_from_hdf(lat).get_aligned(one_hz) best_lon = derived_param_from_hdf(lon).get_aligned(one_hz) # Add KTIs. for kti in kti_list: kti_point_values = {'name': kti.name} if not KEEP_KTIS and kti.name in SKIP_KTIS: continue elif len(KEEP_KTIS)>0 and (kti.name not in KEEP_KTIS): continue altitude = alt.at(kti.index) if alt else None kti_point_values['altitudemode'] = altitude_mode if altitude: kti_point_values['coords'] = ((kti.longitude, kti.latitude, altitude),) else: kti_point_values['coords'] = ((kti.longitude, kti.latitude),) kml.newpoint(**kti_point_values) # Add KPVs. for kpv in kpv_list: # Trap kpvs with invalid latitude or longitude data (normally happens # at the start of the data where accelerometer offsets are declared, # and this avoids casting kpvs into the Atlantic. kpv_lat = best_lat.at(kpv.index) kpv_lon = best_lon.at(kpv.index) if kpv_lat is None or kpv_lon is None or \ (kpv_lat == 0.0 and kpv_lon == 0.0): continue if not KEEP_KPVS and kpv.name in SKIP_KPVS: continue elif len(KEEP_KPVS)>0 and (kpv.name not in KEEP_KPVS): continue style = simplekml.Style() style.iconstyle.color = simplekml.Color.red kpv_point_values = {'name': '%s (%.3f)' % (kpv.name, kpv.value)} altitude = alt.at(kpv.index) if alt else None kpv_point_values['altitudemode'] = altitude_mode if altitude: kpv_point_values['coords'] = ((kpv_lon, kpv_lat, altitude),) else: kpv_point_values['coords'] = ((kpv_lon, kpv_lat),) pnt = kml.newpoint(**kpv_point_values) pnt.style = style # Add approach centre lines. for app in approach_list: try: draw_centreline(kml, app.runway) except: pass if not dest_path: dest_path = hdf_path + ".kml" kml.save(dest_path) return dest_path
def track_to_kml(hdf_path, kti_list, kpv_list, approach_list, plot_altitude='Altitude QNH', dest_path=None): ''' Plot results of process_flight onto a KML track. :param flight_attrs: List of Flight Attributes :type flight_attrs: list :param plot_altitude: Name of Altitude parameter to use in KML :type plot_altitude: String ''' one_hz = Parameter() kml = simplekml.Kml() with hdf_file(hdf_path) as hdf: if 'Latitude Smoothed' not in hdf: return False if plot_altitude not in hdf: logger.warning("Disabling altitude on KML plot as it is unavailable.") plot_altitude = False if plot_altitude: alt = derived_param_from_hdf(hdf[plot_altitude]).get_aligned(one_hz) alt.array = repair_mask(alt.array, frequency=alt.frequency, repair_duration=None) / METRES_TO_FEET else: alt = None if plot_altitude in ['Altitude QNH', 'Altitude AAL', 'Altitude STD']: altitude_mode = simplekml.constants.AltitudeMode.absolute elif plot_altitude in ['Altitude Radio']: altitude_mode = simplekml.constants.AltitudeMode.relativetoground else: altitude_mode = simplekml.constants.AltitudeMode.clamptoground # TODO: align everything to 0 offset smooth_lat = derived_param_from_hdf(hdf['Latitude Smoothed']).get_aligned(one_hz) smooth_lon = derived_param_from_hdf(hdf['Longitude Smoothed']).get_aligned(one_hz) add_track(kml, 'Smoothed', smooth_lat, smooth_lon, 'ff7fff7f', alt_param=alt, alt_mode=altitude_mode) add_track(kml, 'Smoothed On Ground', smooth_lat, smooth_lon, 'ff7fff7f') if 'Latitude Prepared' in hdf and 'Longitude Prepared' in hdf: lat = hdf['Latitude Prepared'] lon = hdf['Longitude Prepared'] add_track(kml, 'Prepared Track', lat, lon, 'A11EB3', visible=False) if 'Latitude' in hdf and 'Longitude' in hdf: lat_r = hdf['Latitude'] lon_r = hdf['Longitude'] # add RAW track default invisible add_track(kml, 'Recorded Track', lat_r, lon_r, 'ff0000ff', visible=False) for kti in kti_list: kti_point_values = {'name': kti.name} if kti.name in SKIP_KTIS: continue altitude = alt.at(kti.index) if plot_altitude else None kti_point_values['altitudemode'] = altitude_mode if altitude: kti_point_values['coords'] = ((kti.longitude, kti.latitude, altitude),) else: kti_point_values['coords'] = ((kti.longitude, kti.latitude),) kml.newpoint(**kti_point_values) for kpv in kpv_list: # Trap kpvs with invalid latitude or longitude data (normally happens # at the start of the data where accelerometer offsets are declared, # and this avoids casting kpvs into the Atlantic. kpv_lat = smooth_lat.at(kpv.index) kpv_lon = smooth_lon.at(kpv.index) if kpv_lat == None or kpv_lon == None or \ (kpv_lat == 0.0 and kpv_lon == 0.0): continue if kpv.name in SKIP_KPVS: continue style = simplekml.Style() style.iconstyle.color = simplekml.Color.red kpv_point_values = {'name': '%s (%.3f)' % (kpv.name, kpv.value)} altitude = alt.at(kpv.index) if plot_altitude else None kpv_point_values['altitudemode'] = altitude_mode if altitude: kpv_point_values['coords'] = ((kpv_lon, kpv_lat, altitude),) else: kpv_point_values['coords'] = ((kpv_lon, kpv_lat),) pnt = kml.newpoint(**kpv_point_values) pnt.style = style for app in approach_list: try: draw_centreline(kml, app.runway) except: pass if not dest_path: dest_path = hdf_path + ".kml" kml.save(dest_path) return
def derive(self, alt_agl=P('Altitude AGL')): repair_array = repair_mask(alt_agl.array, repair_duration=None) hyst_array = hysteresis(repair_array, 10.0) self.array = np.ma.where(alt_agl.array > 10.0, hyst_array, repair_array)
def track_to_kml(hdf_path, kti_list, kpv_list, approach_list, plot_altitude=None, dest_path=None): ''' Plot results of process_flight onto a KML track. :param flight_attrs: List of Flight Attributes :type flight_attrs: list :param plot_altitude: Name of Altitude parameter to use in KML :type plot_altitude: String ''' one_hz = Parameter() kml = simplekml.Kml() with hdf_file(hdf_path) as hdf: # Latitude param, Longitude param, track name, colour coord_params = ( { 'lat': 'Latitude Smoothed', 'lon': 'Longitude Smoothed', 'track': 'Smoothed', 'colour': 'ff7fff7f' }, { 'lat': 'Latitude Prepared', 'lon': 'Longitude Prepared', 'track': 'Prepared', 'colour': 'A11EB3' }, { 'lat': 'Latitude', 'lon': 'Longitude', 'track': 'Recorded', 'colour': 'ff0000ff' }, { 'lat': 'Latitude (Coarse)', 'lon': 'Longitude (Coarse)', 'track': 'Coarse', 'colour': 'ff0000ff' }, ) altitude_absolute_params = ('Altitude QNH', 'Altitude STD', 'Altitude AAL') altitude_relative_params = ('Altitude Radio', ) # Check latitude and longitude pair exist. if not any(c['lat'] in hdf and c['lon'] in hdf for c in coord_params): logger.error( "Cannot write track as coordinate paarmeters not in hdf") return False # Choose best altitude parameter if not specified. if not plot_altitude: altitude_params = itertools.chain(altitude_absolute_params, altitude_relative_params) try: plot_altitude = next(p for p in altitude_params if p in hdf) except StopIteration: logger.warning("Disabling altitude on KML plot as it is " "unavailable.") # Get altitude param from hdf. if plot_altitude: alt = derived_param_from_hdf( hdf[plot_altitude]).get_aligned(one_hz) alt.array = repair_mask(alt.array, frequency=alt.frequency, repair_duration=None) / METRES_TO_FEET else: alt = None if plot_altitude in altitude_absolute_params: altitude_mode = simplekml.constants.AltitudeMode.absolute elif plot_altitude in altitude_relative_params: altitude_mode = simplekml.constants.AltitudeMode.relativetoground else: altitude_mode = simplekml.constants.AltitudeMode.clamptoground ## Get best latitude and longitude parameters. best_lat = None best_lon = None for coord_config in coord_params: lat_name = coord_config['lat'] lon_name = coord_config['lon'] if not lat_name in hdf or not lon_name in hdf: continue lat = hdf[lat_name] lon = hdf[lon_name] best = not best_lat or not best_lon add_track(kml, coord_config['track'], lat, lon, coord_config['colour'], alt_param=alt, alt_mode=altitude_mode, visible=best) add_track(kml, coord_config['track'] + ' On Ground', lat, lon, coord_config['colour'], visible=best) if best: best_lat = derived_param_from_hdf(lat).get_aligned(one_hz) best_lon = derived_param_from_hdf(lon).get_aligned(one_hz) # Add KTIs. for kti in kti_list: kti_point_values = {'name': kti.name} if kti.name in SKIP_KTIS: continue altitude = alt.at(kti.index) if plot_altitude else None kti_point_values['altitudemode'] = altitude_mode if altitude: kti_point_values['coords'] = ((kti.longitude, kti.latitude, altitude), ) else: kti_point_values['coords'] = ((kti.longitude, kti.latitude), ) kml.newpoint(**kti_point_values) # Add KPVs. for kpv in kpv_list: # Trap kpvs with invalid latitude or longitude data (normally happens # at the start of the data where accelerometer offsets are declared, # and this avoids casting kpvs into the Atlantic. kpv_lat = best_lat.at(kpv.index) kpv_lon = best_lon.at(kpv.index) if kpv_lat == None or kpv_lon == None or \ (kpv_lat == 0.0 and kpv_lon == 0.0): continue if kpv.name in SKIP_KPVS: continue style = simplekml.Style() style.iconstyle.color = simplekml.Color.red kpv_point_values = {'name': '%s (%.3f)' % (kpv.name, kpv.value)} altitude = alt.at(kpv.index) if plot_altitude else None kpv_point_values['altitudemode'] = altitude_mode if altitude: kpv_point_values['coords'] = ((kpv_lon, kpv_lat, altitude), ) else: kpv_point_values['coords'] = ((kpv_lon, kpv_lat), ) pnt = kml.newpoint(**kpv_point_values) pnt.style = style # Add approach centre lines. for app in approach_list: try: draw_centreline(kml, app.runway) except: pass if not dest_path: dest_path = hdf_path + ".kml" kml.save(dest_path) return dest_path
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_rad_repaired = repair_mask(alt_rad.array, frequency=alt_rad.frequency, repair_duration=20.0, extrapolate=True) 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 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 derive(self, alt_aal=P('Altitude AAL'), alt_agl=P('Altitude AGL'), ac_type=A('Aircraft Type'), app=S('Approach And Landing'), hdg=P('Heading Continuous'), lat=P('Latitude Prepared'), lon=P('Longitude Prepared'), ils_loc=P('ILS Localizer'), ils_gs=S('ILS Glideslope'), ils_freq=P('ILS Frequency'), land_afr_apt=A('AFR Landing Airport'), land_afr_rwy=A('AFR Landing Runway'), lat_land=KPV('Latitude At Touchdown'), lon_land=KPV('Longitude At Touchdown'), precision=A('Precise Positioning'), fast=S('Fast'), #lat_smoothed=P('Latitude Smoothed'), #lon_smoothed=P('Longitude Smoothed'), u=P('Airspeed'), gspd=P('Groundspeed'), height_from_rig=P('Altitude ADH'), hdot=P('Vertical Speed'), roll=P('Roll'), heading=P('Heading'), distance_land=P('Distance To Landing'), tdwns=KTI('Touchdown'), offshore=M('Offshore'), takeoff=S('Takeoff') ): precise = bool(getattr(precision, 'value', False)) alt = alt_agl if ac_type == helicopter else alt_aal app_slices = sorted(app.get_slices()) for index, _slice in enumerate(app_slices): # a) The last approach is assumed to be landing: if index == len(app_slices) - 1: approach_type = 'LANDING' landing = True # b) We have a touch and go if Altitude AAL reached zero: #elif np.ma.any(alt.array[_slice] <= 0): elif np.ma.any(alt.array[slices_int(_slice.start, _slice.stop+(5*alt.frequency))] <= 0): if ac_type == aeroplane: approach_type = 'TOUCH_AND_GO' landing = False elif ac_type == helicopter: approach_type = 'LANDING' landing = True else: raise ValueError('Not doing hovercraft!') # c) In any other case we have a go-around: else: approach_type = 'GO_AROUND' landing = False # Rough reference index to allow for go-arounds ref_idx = int(index_at_value(alt.array, 0.0, _slice=_slice, endpoint='nearest')) turnoff = None if landing: search_end = fast.get_surrounding(_slice.start) if search_end and search_end[0].slice.stop >= ref_idx: search_end = min(search_end[0].slice.stop, _slice.stop) else: search_end = _slice.stop tdn_hdg = np.ma.median(hdg.array[slices_int(ref_idx, search_end+1)]) # Complex trap for the all landing heading data is masked case... if (tdn_hdg % 360.0) is np.ma.masked: lowest_hdg = bearing_and_distance(lat.array[ref_idx], lon.array[ref_idx], lat.array[search_end], lon.array[search_end])[0] else: lowest_hdg = (tdn_hdg % 360.0).item() # While we're here, let's compute the turnoff index for this landing. head_landing = hdg.array[slices_int((ref_idx+_slice.stop)/2, _slice.stop)] if len(head_landing) > 2: peak_bend = peak_curvature(head_landing, curve_sense='Bipolar') fifteen_deg = index_at_value( np.ma.abs(head_landing - head_landing[0]), 15.0) if peak_bend: turnoff = (ref_idx+_slice.stop)/2 + peak_bend else: if fifteen_deg and fifteen_deg < peak_bend: turnoff = start_search + landing_turn else: # No turn, so just use end of landing run. turnoff = _slice.stop else: # No turn, so just use end of landing run. turnoff = _slice.stop else: # We didn't land, but this is indicative of the runway heading lowest_hdg = (hdg.array[ref_idx] % 360.0).item() # Pass latitude, longitude and heading lowest_lat = None lowest_lon = None if lat and lon and ref_idx: lowest_lat = lat.array[ref_idx] or None lowest_lon = lon.array[ref_idx] or None if lowest_lat and lowest_lon and approach_type == 'GO_AROUND': # Doing a go-around, we extrapolate to the threshold # in case we abort the approach abeam a different airport, # using the rule of three miles per thousand feet. distance = np.ma.array([ut.convert(alt_aal.array[ref_idx] * (3 / 1000.0), ut.NM, ut.METER)]) bearing = np.ma.array([lowest_hdg]) reference = {'latitude': lowest_lat, 'longitude': lowest_lon} lat_ga, lon_ga = latitudes_and_longitudes(bearing, distance, reference) lowest_lat = lat_ga[0] lowest_lon = lon_ga[0] if lat_land and lon_land and not (lowest_lat and lowest_lon): # use lat/lon at landing if values at ref_idx are masked # only interested in landing within approach slice. lat_land = lat_land.get(within_slice=_slice) lon_land = lon_land.get(within_slice=_slice) if lat_land and lon_land: lowest_lat = lat_land[0].value or None lowest_lon = lon_land[0].value or None kwargs = dict( precise=precise, _slice=_slice, lowest_lat=lowest_lat, lowest_lon=lowest_lon, lowest_hdg=lowest_hdg, appr_ils_freq=None, ac_type=ac_type, ) # If the approach is a landing, pass through information from the # achieved flight record in case we cannot determine airport and # runway: if landing: kwargs.update( land_afr_apt=land_afr_apt, land_afr_rwy=land_afr_rwy, hint='landing', ) if landing or approach_type == 'GO_AROUND': # if we have a frequency and valid localiser signal at lowest point in approach appr_ils_freq = None if ils_freq: appr_ils_freq = np.ma.round(ils_freq.array[ref_idx] or 0, 2) if not precise and appr_ils_freq and ils_loc and np.ma.abs(ils_loc.array[ref_idx]) < 2.5: kwargs['appr_ils_freq'] = appr_ils_freq airport, landing_runway = self._lookup_airport_and_runway(**kwargs) if not airport and ac_type == aeroplane: continue if ac_type == aeroplane and not airport.get('runways'): self.error("Airport %s: contains no runways", airport['code']) # Simple determination of heliport. heliport = is_heliport(ac_type, airport, landing_runway) for touchdown, tkoff in zip(tdwns.get_ordered_by_index(), takeoff.get_ordered_by_index()): # If both the takeoff and touchdown point are offshore then we consider # the approach to be a 'SHUTTLING APPROACH'. Else we continue to look for # an 'AIRBORNE RADAR DIRECT/OVERHEAD APPROACH' or a 'STANDARD APPROACH' # # A couple of seconds are added to the end of the slice as some flights used # to test this had the touchdown a couple of seconds outside the approach slice if is_index_within_slice(touchdown.index, slice(_slice.start, _slice.stop+5*alt.frequency)): if offshore and \ offshore.array[int(touchdown.index)] == 'Offshore' and \ tkoff.start < touchdown.index: if not distance_land: if offshore.array[tkoff.start] == 'Offshore': approach_type = 'SHUTTLING' elif offshore.array[tkoff.start] == 'Offshore' and \ tkoff.start < len(distance_land.array) and \ distance_land.array[int(tkoff.start)] <= 40: approach_type = 'SHUTTLING' elif height_from_rig: Vy = 80.0 # Type dependent? # conditions_defs is a dict of condition name : expression to evaluate pairs, listed this way for clarity condition_defs={'Below 120 kts' : lambda p : p['Airspeed'] < 120, 'Below Vy+5' : lambda p : p['Airspeed'] < Vy+5.0, 'Over Vy' : lambda p : p['Airspeed'] > Vy, 'Over Vy-5' : lambda p : p['Airspeed'] > Vy-5.0, 'Below 70 gspd' : lambda p : p['Groundspeed'] < 72, 'Below 60 gspd' : lambda p : p['Groundspeed'] < 60, #'Below Vy-10' : lambda p : p['Airspeed'] < Vy-10.0, #'Over Vy-10' : lambda p : p['Airspeed'] > Vy-10.0, #'Above 30 gspd' : lambda p : p['Groundspeed'] > 30, 'Over 900 ft' : lambda p : p['Altitude ADH'] > 900, 'Over 200 ft' : lambda p : p['Altitude ADH'] > 200, 'Below 1750 ft': lambda p : p['Altitude ADH'] < 1750, 'Below 1100 ft' : lambda p : p['Altitude ADH'] < 1100, 'Over 350 ft' : lambda p : p['Altitude ADH'] > 350, 'Below 700 ft' : lambda p : p['Altitude ADH'] < 700, 'ROD < 700 fpm' : lambda p : p['Vertical Speed'] > -700, 'ROD > 200 fpm' : lambda p : p['Vertical Speed'] < -200, 'Not climbing' : lambda p : p['Vertical Speed'] < 200, #'Over 400 ft' : lambda p : p['Altitude ADH'] > 400, #'Below 1500 ft': lambda p : p['Altitude ADH'] < 1500, #'Below 1300 ft': lambda p : p['Altitude ADH'] < 1300, 'Roll below 25 deg' : lambda p : valid_between(p['Roll'], -25.0, 25.0), 'Wings Level' : lambda p : valid_between(p['Roll'], -10.0, 10.0), 'Within 20 deg of final heading' : lambda p : np.ma.abs(p['head_off_final']) < 20.0, #'Within 45 deg of downwind leg' : 'valid_between(np.ma.abs(head_off_final), 135.0, 225.0)', #'15 deg off final heading' : lambda p : np.ma.abs(np.ma.abs(p['head_off_two_miles'])-15.0) < 5.0, #'Heading towards oil rig' : lambda p : np.ma.abs(p['head_off_two_miles']) < 6.0, 'Beyond 0.7 NM' : lambda p : p['Distance To Landing'] > 0.7, 'Within 0.8 NM' : lambda p : p['Distance To Landing'] < 0.8, 'Beyond 1.5 NM' : lambda p : p['Distance To Landing'] > 1.5, 'Within 2.0 NM' : lambda p : p['Distance To Landing'] < 2.0, 'Within 3.0 NM' : lambda p : p['Distance To Landing'] < 3.0, 'Beyond 3.0 NM' : lambda p : p['Distance To Landing'] > 3.0, 'Within 10.0 NM' : lambda p : p['Distance To Landing'] < 10.0, #'Within 1.5 NM' : lambda p : p['Distance To Landing'] < 1.5, } # Phase map is a dict of the flight phases with the list of conditions which must be # satisfied for the phase to be active. phase_map={'Circuit':['Below 120 kts', 'Over Vy', 'Below 1100 ft', 'Over 900 ft', 'Roll below 25 deg', # includes downwind turn ], 'Level within 2NM':['Below Vy+5', 'Over Vy-5', 'Below 1100 ft', 'Over 900 ft', 'Wings Level', 'Within 20 deg of final heading', 'Within 2.0 NM', 'Beyond 1.5 NM', ], 'Initial Descent':['Wings Level', 'Within 20 deg of final heading', 'ROD < 700 fpm', 'ROD > 200 fpm', 'Beyond 0.7 NM', 'Over 350 ft', ], 'Final Approach':['Wings Level', 'Within 20 deg of final heading', 'ROD < 700 fpm', 'Within 0.8 NM', 'Below 60 gspd', 'Below 700 ft', ], # Phases for ARDA/AROA # # All heading conditions are commented out as the pilots usually # go outside the boundaries; the other conditions seem to be # enough to detect them 'ARDA/AROA 10 to 3':['Within 10.0 NM', 'Beyond 3.0 NM', 'Below 1750 ft', 'Not climbing', #'Heading towards oil rig', ], 'ARDA/AROA Level within 3NM':['Below 70 gspd', 'Over 200 ft', 'Wings Level', 'Within 3.0 NM', 'Beyond 1.5 NM', #'Within 20 deg of final heading', ], 'ARDA/AROA Final':['Not climbing', 'Within 2.0 NM', #'15 deg off final heading' ], } """ #Phases that can be used to tighten the conditions for ARDA/AROA 'Radar Heading Change':['15 deg off final heading', 'Within 1.5 NM', 'Beyond 0.7 NM'], 'Low Approach':['Below Vy+5', 'Below 700 ft', 'Over 350 ft', 'Within 20 deg of final heading', 'Wings Level'], 'Low Circuit':['Below 120 kts', 'Over Vy-5', 'Below 700 ft', 'Over 350 ft', 'Roll below 25 deg'] """ approach_map = {'RIG': ['Circuit', 'Level within 2NM', 'Initial Descent', 'Final Approach'], 'AIRBORNE_RADAR': ['ARDA/AROA 10 to 3', 'ARDA/AROA Level within 3NM', 'ARDA/AROA Final']} # Making sure the approach slice contains enough information to be able # to properly identify ARDA/AROA approaches (the procedure starts from 10NM # before touchdown) app_slice = slice(index_at_value(distance_land.array, 11, _slice=slice(0,touchdown.index)), touchdown.index) heading_repaired = repair_mask(heading.array[app_slice], frequency=heading.frequency, repair_duration=np.ma.count_masked(heading.array[app_slice]), copy=True, extrapolate=True) param_arrays = { 'Airspeed': u.array[app_slice], 'Groundspeed': gspd.array[app_slice], 'Altitude ADH': height_from_rig.array[app_slice], 'Vertical Speed': hdot.array[app_slice], 'Roll': roll.array[app_slice], 'Distance To Landing': distance_land.array[app_slice], 'Heading': heading_repaired, 'Latitude': lat.array[app_slice], 'Longitude': lon.array[app_slice], } longest_approach_type, longest_approach_durn, longest_approach_slice = find_rig_approach(condition_defs, phase_map, approach_map, Vy, None, param_arrays, debug=False) if longest_approach_type is not None: approach_type = longest_approach_type.upper() _slice = slice(app_slice.start + longest_approach_slice.start, app_slice.stop) if heliport: self.create_approach( approach_type, _slice, runway_change=False, offset_ils=False, airport=airport, landing_runway=None, approach_runway=None, gs_est=None, loc_est=None, ils_freq=None, turnoff=None, lowest_lat=lowest_lat, lowest_lon=lowest_lon, lowest_hdg=lowest_hdg, ) continue ######################################################################### ## Analysis of fixed wing approach to a runway ## ## First step is to check the ILS frequency for the runway in use ## and cater for a change from the approach runway to the landing runway. ######################################################################### appr_ils_freq = None runway_change = False offset_ils = False # Do we have a recorded ILS frequency? If so, what was it tuned to at the start of the approach?? if ils_freq: appr_ils_freq = ils_freq.array[int(_slice.start)] # Was this valid, and if so did the start of the approach match the landing runway? if appr_ils_freq and not (np.isnan(appr_ils_freq) or np.ma.is_masked(appr_ils_freq)): appr_ils_freq = round(appr_ils_freq, 2) runway_kwargs = { 'ilsfreq': appr_ils_freq, 'latitude': lowest_lat, 'longitude': lowest_lon, } if not precise: runway_kwargs['hint'] = kwargs.get('hint', 'approach') approach_runway = nearest_runway(airport, lowest_hdg, **runway_kwargs) # Have we have identified runways for both conditions that are both different and parallel? if all((approach_runway, landing_runway)) \ and approach_runway['id'] != landing_runway['id'] \ and approach_runway['identifier'][:2] == landing_runway['identifier'][:2]: runway_change = True else: # Without a frequency source, we just have to hope any localizer signal is for this runway! approach_runway = landing_runway if approach_runway and 'frequency' in approach_runway['localizer']: if np.ma.count(ils_loc.array[slices_int(_slice)]) > 10: if runway_change: # We only use the first frequency tuned. This stops scanning across both runways if the pilot retunes. loc_slice = shift_slices( runs_of_ones(np.ma.abs(ils_freq.array[slices_int(_slice)] - appr_ils_freq) < 0.001), _slice.start )[0] else: loc_slice = _slice else: # No localizer or inadequate data for this approach. loc_slice = None else: # The approach was to a runway without an ILS, so even if it was tuned, we ignore this. appr_ils_freq = None loc_slice = None if np.ma.is_masked(appr_ils_freq): loc_slice = None appr_ils_freq = None else: if appr_ils_freq and loc_slice: if appr_ils_freq != round(ut.convert(approach_runway['localizer']['frequency'], ut.KHZ, ut.MHZ), 2): loc_slice = None ####################################################################### ## Identification of the period established on the localizer ####################################################################### loc_est = None if loc_slice: valid_range = np.ma.flatnotmasked_edges(ils_loc.array[slices_int(_slice)]) # I have some data to scan. Shorthand names; loc_start = valid_range[0] + _slice.start loc_end = valid_range[1] + _slice.start scan_back = slice(ref_idx, loc_start, -1) # If we are turning in, we are not interested in signals that are not related to this approach. # The value of 45 deg was selected to encompass Washington National airport with a 40 deg offset. hdg_diff = np.ma.abs(np.ma.mod((hdg.array-lowest_hdg)+180.0, 360.0)-180.0) ils_hdg_45 = index_at_value(hdg_diff, 45.0, _slice=scan_back) # We are not interested above 1,500 ft, so may trim back the start point to that point: ils_alt_1500 = index_at_value(alt_aal.array, 1500.0, _slice=scan_back) # The criteria for start of established phase is the latter of the approach phase start, the turn-in or 1500ft. # The "or 0" allow for flights that do not turn through 45 deg or keep below 1500ft. loc_start = max(loc_start, ils_hdg_45 or 0, ils_alt_1500 or 0) if loc_start < ref_idx: # Did I get established on the localizer, and if so, # when? We only look AFTER the aircraft is already within # 45deg of the runway heading, below 1500ft and the data # is valid for this runway. Testing that the aircraft is # not just passing across the localizer is built into the # ils_established function. loc_estab = ils_established(ils_loc.array, slice(loc_start, ref_idx), ils_loc.hz) else: # If localiser start is after we touchdown bail. loc_estab = None if loc_estab: # Refine the end of the localizer established phase... if (approach_runway and approach_runway['localizer']['is_offset']): offset_ils = True # The ILS established phase ends when the deviation becomes large. loc_end = ils_established(ils_loc.array, slice(ref_idx, loc_estab, -1), ils_loc.hz, point='immediate') elif approach_type in ['TOUCH_AND_GO', 'GO_AROUND']: # We finish at the lowest point loc_end = ref_idx elif runway_change: # Use the end of localizer phase as this already reflects the tuned frequency. est_end = ils_established(ils_loc.array, slice(loc_estab, ref_idx), ils_loc.hz, point='end') # Make sure we dont end up with a negative slice i.e. end before we are established. loc_end = min([x for x in (loc_slice.stop, loc_end, est_end or np.inf) if x > loc_estab]) elif approach_type == 'LANDING': # Just end at 2 dots where we turn off the runway loc_end_2_dots = index_at_value(np.ma.abs(ils_loc.array), 2.0, _slice=slice(turnoff+5*(_slice.stop-_slice.start)/100, loc_estab, -1)) if loc_end_2_dots and \ is_index_within_slice(loc_end_2_dots, _slice) and \ not np.ma.is_masked(ils_loc.array[int(loc_end_2_dots)]) and \ loc_end_2_dots > loc_estab: loc_end = loc_end_2_dots loc_est = slice(loc_estab, loc_end+1) ####################################################################### ## Identification of the period established on the glideslope ####################################################################### gs_est = None if loc_est and 'glideslope' in approach_runway and ils_gs: # We only look for glideslope established periods if the localizer is already established. # The range to scan for the glideslope starts with localizer capture and ends at # 200ft or the minimum height point for a go-around or the end of # localizer established, if either is earlier. ils_gs_start = loc_estab ils_gs_200 = index_at_value(alt.array, 200.0, _slice=slice(loc_end, ils_gs_start, -1)) # The expression "ils_gs_200 or np.inf" caters for the case where the aircraft did not pass # through 200ft, so the result is None, in which case any other value is left to be the minimum. ils_gs_end = min(ils_gs_200 or np.inf, ref_idx, loc_end) # Look for ten seconds within half a dot ils_gs_estab = ils_established(ils_gs.array, slice(ils_gs_start, ils_gs_end), ils_gs.hz) if ils_gs_estab: gs_est = slice(ils_gs_estab, ils_gs_end+1) ''' # These statements help set up test cases. print() print(airport['name']) print(approach_runway['identifier']) print(landing_runway['identifier']) print(_slice) if loc_est: print('Localizer established ', loc_est.start, loc_est.stop) if gs_est: print('Glideslope established ', gs_est.start, gs_est.stop) print() ''' self.create_approach( approach_type, _slice, runway_change=runway_change, offset_ils=offset_ils, airport=airport, landing_runway=landing_runway, approach_runway=approach_runway, gs_est=gs_est, loc_est=loc_est, ils_freq=appr_ils_freq, turnoff=turnoff, lowest_lat=lowest_lat, lowest_lon=lowest_lon, lowest_hdg=lowest_hdg, )
def split_segments(hdf): ''' 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? ''' airspeed = hdf['Airspeed'] # 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 Airspeed try: airspeed_array = repair_mask(airspeed.array, repair_duration=None, repair_above=settings.AIRSPEED_THRESHOLD) except ValueError: # Airspeed array is masked, most likely under min threshold so it did # not go fast. logger.warning("Airspeed is entirely masked. The entire contents of " "the data will be a GROUND_ONLY slice.") return [_segment_type_and_slice( airspeed.array, airspeed.frequency, heading.array, heading.frequency, 0, hdf.duration, eng_arrays)] airspeed_secs = len(airspeed_array) / airspeed.frequency slow_array = np.ma.masked_less_equal(airspeed_array, settings.AIRSPEED_THRESHOLD) speedy_slices = np.ma.clump_unmasked(slow_array) if len(speedy_slices) <= 1: logger.info("There are '%d' sections of data where airspeed 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( airspeed_array, airspeed.frequency, heading.array, heading.frequency, 0, airspeed_secs, eng_arrays)] slow_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(airspeed_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) / airspeed.frequency if fast_duration < settings.MINIMUM_FAST_DURATION: logger.info("Disregarding short period of fast airspeed %s", fast_duration) continue # Get start and stop at 1Hz. slice_start_secs = slow_slice.start / airspeed.frequency slice_stop_secs = slow_slice.stop / airspeed.frequency slow_duration = slice_stop_secs - slice_start_secs if slow_duration < settings.MINIMUM_SPLIT_DURATION: logger.info("Disregarding period of airspeed below '%s' " "since '%s' is shorter than MINIMUM_SPLIT_DURATION " "('%s').", settings.AIRSPEED_THRESHOLD, slow_duration, settings.MINIMUM_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( airspeed_array, airspeed.frequency, heading.array, heading.frequency, start, dfc_split_index, eng_arrays)) 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 MINIMUM_SPLIT_PARAM_VALUE ('%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( airspeed_array, airspeed.frequency, heading.array, heading.frequency, start, eng_split_index, eng_arrays)) 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( airspeed_array, airspeed.frequency, heading.array, heading.frequency, start, rot_split_index, eng_arrays)) 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( airspeed_array, airspeed.frequency, heading.array, heading.frequency, start, airspeed_secs, eng_arrays)) return segments
def split_segments(hdf): ''' 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? ''' airspeed = hdf['Airspeed'] try: airspeed_array = repair_mask(airspeed.array, repair_duration=None, repair_above=settings.AIRSPEED_THRESHOLD) except ValueError: # Airspeed array is masked, most likely under min threshold so it did # not go fast. logger.warning("Airspeed is entirely masked. The entire contents of " "the data will be a GROUND_ONLY slice.") #TODO: Return "NO_MOVEMENT" when supported return [('GROUND_ONLY', slice(0, hdf.duration))] 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) airspeed_secs = len(airspeed_array) / airspeed.frequency slow_array = np.ma.masked_less_equal(airspeed_array, settings.AIRSPEED_THRESHOLD) speedy_slices = np.ma.clump_unmasked(slow_array) if len(speedy_slices) <= 1: logger.info("There are '%d' sections of data where airspeed 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( airspeed_array, airspeed.frequency, heading.array, heading.frequency, 0, airspeed_secs)] slow_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(airspeed_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) / airspeed.frequency if fast_duration < settings.MINIMUM_FAST_DURATION: logger.info("Disregarding short period of fast airspeed %s", fast_duration) continue # Get start and stop at 1Hz. slice_start_secs = slow_slice.start / airspeed.frequency slice_stop_secs = slow_slice.stop / airspeed.frequency slow_duration = slice_stop_secs - slice_start_secs if slow_duration < settings.MINIMUM_SPLIT_DURATION: logger.info("Disregarding period of airspeed below '%s' " "since '%s' is shorter than MINIMUM_SPLIT_DURATION " "('%s').", settings.AIRSPEED_THRESHOLD, slow_duration, settings.MINIMUM_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( airspeed_array, airspeed.frequency, heading.array, heading.frequency, start, dfc_split_index)) 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 MINIMUM_SPLIT_PARAM_VALUE ('%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( airspeed_array, airspeed.frequency, heading.array, heading.frequency, start, eng_split_index)) 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( airspeed_array, airspeed.frequency, heading.array, heading.frequency, start, rot_split_index)) start = rot_split_index logger.info("Splitting at index '%s' where rate of turn was below " "'%s'.", rot_split_index, settings.RATE_OF_TURN_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( airspeed_array, airspeed.frequency, heading.array, heading.frequency, start, airspeed_secs)) return segments