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, tcas=M('TCAS Combined Control') ): #, off=KTI('Liftoff'), td=KTI('Touchdown') ): ras_local = tcas.array.any_of('Drop Track', 'Altitude Lost', 'Up Advisory Corrective','Down Advisory Corrective') ras_slices = library.runs_of_ones(ras_local) if ras_slices: for ra_slice in ras_slices: self.create_phase( ra_slice ) return
def append_segment_info(hdf_segment_path, segment_type, segment_slice, part, fallback_dt=None): """ Get information about a segment such as type, hash, etc. and return a named tuple. If a valid timestamp can't be found, it creates start_dt as epoch(0) i.e. datetime(1970,1,1,1,0). Go-fast dt and Stop dt are relative to this point in time. :param hdf_segment_path: path to HDF segment to analyse :type hdf_segment_path: string :param segment_slice: Slice of this segment relative to original file. :type segment_slice: slice :param part: Numeric part this segment was in the original data file (1 indexed) :type part: Integer :param fallback_dt: Used to replace elements of datetimes which are not available in the hdf file (e.g. YEAR not being recorded) :type fallback_dt: datetime :returns: Segment named tuple :rtype: Segment """ # build information about a slice with hdf_file(hdf_segment_path) as hdf: airspeed = hdf['Airspeed'].array duration = hdf.duration # For now, raise TimebaseError up rather than using EPOCH # TODO: Review whether to revert to epoch again. ##try: start_datetime = _calculate_start_datetime(hdf, fallback_dt) ##except TimebaseError: ##logger.warning("Unable to calculate timebase, using epoch " ##"1.1.1970!") ##start_datetime = datetime.fromtimestamp(0) stop_datetime = start_datetime + timedelta(seconds=duration) hdf.start_datetime = start_datetime if segment_type in ('START_AND_STOP', 'START_ONLY', 'STOP_ONLY'): # we went fast, so get the index spd_above_threshold = \ np.ma.where(airspeed > settings.AIRSPEED_THRESHOLD) go_fast_index = spd_above_threshold[0][0] go_fast_datetime = \ start_datetime + timedelta(seconds=int(go_fast_index)) # Identification of raw data airspeed hash airspeed_hash_sections = runs_of_ones(airspeed.data > settings.AIRSPEED_THRESHOLD) airspeed_hash = hash_array(airspeed.data,airspeed_hash_sections, settings.AIRSPEED_HASH_MIN_SAMPLES) #elif segment_type == 'GROUND_ONLY': ##Q: Create a groundspeed hash? #pass else: go_fast_index = None go_fast_datetime = None # if not go_fast, create hash from entire file airspeed_hash = sha_hash_file(hdf_segment_path) # ('slice type part path hash start_dt go_fast_dt stop_dt') segment = Segment(segment_slice, segment_type, part, hdf_segment_path, airspeed_hash, start_datetime, go_fast_datetime, stop_datetime) return segment
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, ra=M('TCAS RA'), off=KTI('Liftoff'), td=KTI('Touchdown') ): ras_local = ra.array ras_slices = library.runs_of_ones(ras_local) # put together runs separated by short drop-outs ras_slicesb = library.slices_remove_small_gaps(ras_slices, time_limit=2, hz=1) for ra_slice in ras_slicesb: is_post_liftoff = (ra_slice.start - off.get_first().index) > 10 is_pre_touchdown = (td.get_first().index - ra_slice.start ) > 10 duration = ra_slice.stop-ra_slice.start if is_post_liftoff and is_pre_touchdown and 3.0 <= duration < 300.0: #ignore if too short to do anything #print ' ra section', ra_slice self.create_phase( ra_slice ) return
def derive(self, gl=M('Gear (L) Up'), gn=M('Gear (N) Up'), gr=M('Gear (R) Up'), gc=M('Gear (C) Up')): # Join all available gear parameters and use whichever are available. self.array = vstack_params_where_state( (gl, 'Up'), (gn, 'Up'), (gr, 'Up'), (gc, 'Up'), ).all(axis=0) # remove any spikes _slices = runs_of_ones(self.array == 'Down') _slices = slices_remove_small_gaps(_slices, 2, self.hz) for _slice in _slices: self.array[_slice] = 'Down'
def _split_on_eng_params(slice_start_secs, slice_stop_secs, split_params_min, split_params_frequency): ''' Find split using engine parameters. :param slice_start_secs: Start of slow slice in seconds. :type slice_start_secs: int or float :param slice_stop_secs: Stop of slow slice in seconds. :type slice_stop_secs: int or float :param split_params_min: Minimum of engine parameters. :type split_params_min: np.ma.MaskedArray :param split_params_frequency: Frequency of split_params_min. :type split_params_frequency: int or float :returns: Split index in seconds and value of split_params_min at this index. :rtype: (int or float, int or float) ''' slice_start = slice_start_secs * split_params_frequency slice_stop = slice_stop_secs * split_params_frequency split_params_slice = slice(np.round(slice_start, 0), np.round(slice_stop, 0)) split_index, split_value = min_value(split_params_min, _slice=split_params_slice) if split_index is None: return split_index, split_value eng_min_slices = slices_remove_small_slices(slices_remove_small_gaps( runs_of_ones(split_params_min[split_params_slice] == split_value), time_limit=60, hz=split_params_frequency), hz=split_params_frequency) if not eng_min_slices: return split_index, split_value split_index = eng_min_slices[0].start + \ ((eng_min_slices[0].stop - eng_min_slices[0].start) / 2) + slice_start split_index = round(split_index / split_params_frequency) return split_index, split_value
def derive(self, rotors=M('Rotors Running')): self.create_sections(runs_of_ones(rotors.array == 'Running'))
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, 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 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 append_segment_info(hdf_segment_path, segment_type, segment_slice, part, fallback_dt=None, validation_dt=None, aircraft_info={}): """ Get information about a segment such as type, hash, etc. and return a named tuple. If a valid timestamp can't be found, it creates start_dt as epoch(0) i.e. datetime(1970,1,1,1,0). Go-fast dt and Stop dt are relative to this point in time. :param hdf_segment_path: path to HDF segment to analyse :type hdf_segment_path: string :param segment_slice: Slice of this segment relative to original file. :type segment_slice: slice :param part: Numeric part this segment was in the original data file (1 indexed) :type part: Integer :param fallback_dt: Used to replace elements of datetimes which are not available in the hdf file (e.g. YEAR not being recorded) :type fallback_dt: datetime :returns: Segment named tuple :rtype: Segment """ # build information about a slice with hdf_file(hdf_segment_path) as hdf: speed, thresholds = _get_speed_parameter(hdf, aircraft_info) duration = hdf.duration try: start_datetime = _calculate_start_datetime(hdf, fallback_dt, validation_dt) except TimebaseError: # Warn the user and store the fake datetime. The code on the other # side should check the datetime and avoid processing this file logger.exception( 'Unable to calculate timebase, using 1970-01-01 00:00:00+0000!' ) start_datetime = datetime.utcfromtimestamp(0).replace( tzinfo=pytz.utc) stop_datetime = start_datetime + timedelta(seconds=duration) hdf.start_datetime = start_datetime if segment_type in ('START_AND_STOP', 'START_ONLY', 'STOP_ONLY'): # we went fast, so get the index spd_above_threshold = \ np.ma.where(speed.array > thresholds['speed_threshold']) go_fast_index = spd_above_threshold[0][0] / speed.frequency go_fast_datetime = \ start_datetime + timedelta(seconds=int(go_fast_index)) # Identification of raw data speed hash speed_hash_sections = runs_of_ones( speed.array.data > thresholds['speed_threshold']) speed_hash = hash_array(speed.array.data, speed_hash_sections, thresholds['hash_min_samples']) #elif segment_type == 'GROUND_ONLY': ##Q: Create a groundspeed hash? #pass else: go_fast_index = None go_fast_datetime = None # if not go_fast, create hash from entire file speed_hash = sha_hash_file(hdf_segment_path) segment = Segment(segment_slice, segment_type, part, hdf_segment_path, speed_hash, start_datetime, go_fast_datetime, stop_datetime) return segment
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'), ): precise = bool(getattr(precision, 'value', False)) alt = alt_agl if ac_type == helicopter else alt_aal app_slices = 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): 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: 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]) 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] 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: # 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 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, ) # 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. # This function may be expanded to cater for rig approaches in future. heliport = is_heliport(ac_type, airport, landing_runway) 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 = round(ils_freq.array[_slice.start], 2) # 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): 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 approach_runway['localizer'].has_key('frequency'): 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 appr_ils_freq and loc_slice: if appr_ils_freq != approach_runway['localizer']['frequency']/1000.0: 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') loc_end = min(loc_slice.stop, loc_end, est_end or np.inf) 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(loc_end, loc_estab, -1)) if loc_end_2_dots: 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 approach_runway.has_key('glideslope') 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, 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 append_segment_info(hdf_segment_path, segment_type, segment_slice, part, fallback_dt=None): """ Get information about a segment such as type, hash, etc. and return a named tuple. If a valid timestamp can't be found, it creates start_dt as epoch(0) i.e. datetime(1970,1,1,1,0). Go-fast dt and Stop dt are relative to this point in time. :param hdf_segment_path: path to HDF segment to analyse :type hdf_segment_path: string :param segment_slice: Slice of this segment relative to original file. :type segment_slice: slice :param part: Numeric part this segment was in the original data file (1 indexed) :type part: Integer :param fallback_dt: Used to replace elements of datetimes which are not available in the hdf file (e.g. YEAR not being recorded) :type fallback_dt: datetime :returns: Segment named tuple :rtype: Segment """ # build information about a slice with hdf_file(hdf_segment_path) as hdf: airspeed = hdf['Airspeed'] duration = hdf.duration try: start_datetime = _calculate_start_datetime(hdf, fallback_dt) except TimebaseError: # Warn the user and store the fake datetime. The code on the other # side should check the datetime and avoid processing this file logger.exception('Unable to calculate timebase, using ' '1970-01-01 00:00:00+0000!') start_datetime = datetime.utcfromtimestamp(0).replace(tzinfo=pytz.utc) stop_datetime = start_datetime + timedelta(seconds=duration) hdf.start_datetime = start_datetime if segment_type in ('START_AND_STOP', 'START_ONLY', 'STOP_ONLY'): # we went fast, so get the index spd_above_threshold = \ np.ma.where(airspeed.array > settings.AIRSPEED_THRESHOLD) go_fast_index = spd_above_threshold[0][0] / airspeed.frequency go_fast_datetime = \ start_datetime + timedelta(seconds=int(go_fast_index)) # Identification of raw data airspeed hash airspeed_hash_sections = runs_of_ones(airspeed.array.data > settings.AIRSPEED_THRESHOLD) airspeed_hash = hash_array(airspeed.array.data, airspeed_hash_sections, settings.AIRSPEED_HASH_MIN_SAMPLES) #elif segment_type == 'GROUND_ONLY': ##Q: Create a groundspeed hash? #pass else: go_fast_index = None go_fast_datetime = None # if not go_fast, create hash from entire file airspeed_hash = sha_hash_file(hdf_segment_path) segment = Segment( segment_slice, segment_type, part, hdf_segment_path, airspeed_hash, start_datetime, go_fast_datetime, stop_datetime ) return segment
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