def test_nose_down_multiple_climbs(self): node = NoseDownAttitudeAdoption() pitch = np.concatenate([np.ones(15) * 2, np.linspace(2, -11, num=15), np.linspace(-11, 2, num=10), np.ones(20) * 2, np.linspace(2, -11, num=15), np.ones(10) * -11]) climbs = buildsections('Initial Climb', [10, 40], [60, 85]) node.derive(P('Pitch', pitch), climbs) self.assertEqual(len(node), 2) self.assertEqual(node[0], Section('Nose Down Attitude Adoption', slice(15, 28, None), 15, 28)) self.assertEqual(node[1], Section('Nose Down Attitude Adoption', slice(60, 73, None), 60, 73))
def buildsection(name, begin, end): '''from FlightDataAnalyzer tests A little routine to make building Sections for testing easier. Example: land = buildsection('Landing', 100, 120) ''' result = Section(name, slice(begin, end, None), begin, end) return SectionNode(name, items=[result])
def setUp(self): self.node_class = ApproachInformation self.alt_aal = P(name='Altitude AAL', array=np.ma.array([ 10, 5, 0, 0, 5, 10, 20, 30, 40, 50, # Touch & Go 50, 45, 30, 35, 30, 30, 35, 40, 40, 40, # Go Around 30, 20, 10, 0, 0, 0, 0, 0, 0, 0, # Landing ])) self.app = ApproachAndLanding() self.fast = S(name='Fast', items=[ Section(name='Fast', slice=slice(0, 22), start_edge=0, stop_edge=22.5), ]) self.land_hdg = KPV(name='Heading During Landing', items=[ KeyPointValue(index=22, value=60), ]) self.land_lat = KPV(name='Latitude At Touchdown', items=[ KeyPointValue(index=22, value=10), ]) self.land_lon = KPV(name='Longitude At Touchdown', items=[ KeyPointValue(index=22, value=-2), ]) self.appr_hdg = KPV(name='Heading At Lowest Altitude During Approach', items=[ KeyPointValue(index=5, value=25), KeyPointValue(index=12, value=35), ]) self.appr_lat = KPV(name='Latitude At Lowest Altitude During Approach', items=[ KeyPointValue(index=5, value=8), ]) self.appr_lon = KPV(name='Longitude At Lowest Altitude During Approach', items=[ KeyPointValue(index=5, value=4), ]) self.land_afr_apt_none = A(name='AFR Landing Airport', value=None) self.land_afr_rwy_none = A(name='AFR Landing Runway', value=None)
def align_section(result, duration, section_list): aligned_section = result.get_aligned(P(frequency=1, offset=0)) for index, one_hz in enumerate(aligned_section): # SectionNodes allow slice starts and stops being None which # signifies the beginning and end of the data. To avoid TypeErrors # in subsequent derive methods which perform arithmetic on section # slice start and stops, replace with 0 or hdf.duration. fallback = lambda x, y: x if x is not None else y duration = fallback(duration, 0) start = fallback(one_hz.slice.start, 0) stop = fallback(one_hz.slice.stop, duration) start_edge = fallback(one_hz.start_edge, 0) stop_edge = fallback(one_hz.stop_edge, duration) slice_ = slice(start, stop) one_hz = Section(one_hz.name, slice_, start_edge, stop_edge) aligned_section[index] = one_hz if not (0 <= start <= duration and 0 <= stop <= duration + 1): msg = "Section '%s' (%.2f, %.2f) not between 0 and %d" raise IndexError(msg % (one_hz.name, start, stop, duration)) if not 0 <= start_edge <= duration: msg = "Section '%s' start_edge (%.2f) not between 0 and %d" raise IndexError(msg % (one_hz.name, start_edge, duration)) if not 0 <= stop_edge <= duration + 1: msg = "Section '%s' stop_edge (%.2f) not between 0 and %d" raise IndexError(msg % (one_hz.name, stop_edge, duration)) section_list.append(one_hz) return aligned_section, section_list
def test_nose_down_insufficient_pitch(self): node = NoseDownAttitudeAdoption() pitch = np.concatenate([np.ones(15) * 2, np.linspace(2, -6, num=15), np.ones(10) * -6]) node.derive(P('Pitch', pitch), self.climbs) self.assertEqual(len(node), 1) self.assertEqual(node[0], Section('Nose Down Attitude Adoption', slice(15, 29, None), 15, 29))
def buildsections(*args): '''from FlightDataAnalyzer tests Example: approach = buildsections('Approach', [80,90], [100,110]) ''' built_list = [] name = args[0] for a in args[1:]: begin, end = a[0], a[1] new_section = Section(name, slice(begin, end, None), begin, end) built_list.append(new_section) return SectionNode(name, items=built_list)
def test_nose_down_basic(self): node = NoseDownAttitudeAdoption() pitch = np.concatenate( [np.ones(15) * 2, np.linspace(2, -11, num=15), np.ones(10) * -11]) node.derive(P('Pitch', pitch), self.climbs, self.offshore) self.assertEqual(len(node), 1) self.assertEqual( node[0], Section('Nose Down Attitude Adoption', slice(15, 28, None), 15, 28))
def test__controls_in_use(self): pitch_capt = Mock() pitch_fo = Mock() roll_capt = Mock() roll_fo = Mock() section = Section('Takeoff', slice(0, 3), 0, 3) # Note: We instantiate one of the subclasses of DeterminePilot as we # use logging methods not defined in this abstract superclass. determine_pilot = LandingPilot() determine_pilot._controls_changed = Mock() # Neither pilot's controls changed: determine_pilot._controls_changed.reset_mock() determine_pilot._controls_changed.side_effect = [False, False] pilot = determine_pilot._controls_in_use(pitch_capt, pitch_fo, roll_capt, roll_fo, section) determine_pilot._controls_changed.assert_has_calls([ call(section.slice, pitch_capt, roll_capt), call(section.slice, pitch_fo, roll_fo), ]) self.assertEqual(pilot, None) # Only captain's controls changed: determine_pilot._controls_changed.reset_mock() determine_pilot._controls_changed.side_effect = [True, False] pilot = determine_pilot._controls_in_use(pitch_capt, pitch_fo, roll_capt, roll_fo, section) determine_pilot._controls_changed.assert_has_calls([ call(section.slice, pitch_capt, roll_capt), call(section.slice, pitch_fo, roll_fo), ]) self.assertEqual(pilot, 'Captain') # Only first Officer's controls changed: determine_pilot._controls_changed.reset_mock() determine_pilot._controls_changed.side_effect = [False, True] pilot = determine_pilot._controls_in_use(pitch_capt, pitch_fo, roll_capt, roll_fo, section) determine_pilot._controls_changed.assert_has_calls([ call(section.slice, pitch_capt, roll_capt), call(section.slice, pitch_fo, roll_fo), ]) self.assertEqual(pilot, 'First Officer') # Both pilot's controls changed: determine_pilot._controls_changed.reset_mock() determine_pilot._controls_changed.side_effect = [True, True] pilot = determine_pilot._controls_in_use(pitch_capt, pitch_fo, roll_capt, roll_fo, section) determine_pilot._controls_changed.assert_has_calls([ call(section.slice, pitch_capt, roll_capt), call(section.slice, pitch_fo, roll_fo), ]) self.assertEqual(pilot, None)
def builditem(name, begin, end, start_edge=None, stop_edge=None): ''' This code more accurately represents the aligned section values, but is not suitable for test cases where the data does not get aligned. if begin is None: ib = None else: ib = int(begin) if ib < begin: ib += 1 if end is None: ie = None else: ie = int(end) if ie < end: ie += 1 :param begin: index at start of section :param end: index at end of section ''' slice_end = end if end is None else end + 1 return Section(name, slice(begin, slice_end, None), start_edge or begin, stop_edge or end)
def derive_parameters(hdf, node_mgr, process_order, params=None, force=False): ''' Derives parameters in process_order. Dependencies are sourced via the node_mgr. :param hdf: Data file accessor used to get and save parameter data and attributes :type hdf: hdf_file :param node_mgr: Used to determine the type of node in the process_order :type node_mgr: NodeManager :param process_order: Parameter / Node class names in the required order to be processed :type process_order: list of strings ''' if not params: params = {} # OPT: local lookup is faster than module-level (small). node_subclasses = NODE_SUBCLASSES # store all derived params that aren't masked arrays approaches = {} # duplicate storage, but maintaining types kpvs = {} ktis = {} # 'Node Name' : node() pass in node.get_accessor() sections = {} flight_attrs = {} # cache of nodes to avoid repeated array alignment cache = {} if NODE_CACHE else None duration = hdf.duration for param_name in process_order: if param_name in node_mgr.hdf_keys: continue elif param_name in params: node = params[param_name] # populate output already at 1Hz if node.node_type is KeyPointValueNode: kpvs[param_name] = list(node) elif node.node_type is KeyTimeInstanceNode: ktis[param_name] = list(node) elif node.node_type is FlightAttributeNode: flight_attrs[param_name] = [Attribute(node.name, node.value)] elif node.node_type is SectionNode: sections[param_name] = list(node) # DerivedParameterNodes are not supported in initial data. continue elif node_mgr.get_attribute(param_name) is not None: # add attribute to dictionary of available params ###params[param_name] = node_mgr.get_attribute(param_name) #TODO: optimise with only one call to get_attribute continue #NB raises KeyError if Node is "unknown" node_class = node_mgr.derived_nodes[param_name] # build ordered dependencies deps = [] node_deps = node_class.get_dependency_names() for dep_name in node_deps: if dep_name in params: # already calculated KPV/KTI/Phase deps.append(params[dep_name]) elif node_mgr.get_attribute(dep_name) is not None: deps.append(node_mgr.get_attribute(dep_name)) elif dep_name in node_mgr.hdf_keys: # LFL/Derived parameter # all parameters (LFL or other) need get_aligned which is # available on DerivedParameterNode try: dp = derived_param_from_hdf(hdf.get_param(dep_name, valid_only=True), cache=cache) except KeyError: # Parameter is invalid. dp = None deps.append(dp) else: # dependency not available deps.append(None) if all([d is None for d in deps]): raise RuntimeError("No dependencies available - Nodes cannot " "operate without ANY dependencies available! " "Node: %s" % node_class.__name__) # initialise node node = node_class(cache=cache) # shhh, secret accessors for developing nodes in debug mode node._p = params node._h = hdf node._n = node_mgr logger.debug("Processing %s `%s`", get_node_type(node, node_subclasses), param_name) # Derive the resulting value try: node = node.get_derived(deps) except: if not force: raise del node._p del node._h del node._n if node.node_type is KeyPointValueNode: params[param_name] = node aligned_kpvs = [] for one_hz in node.get_aligned(P(frequency=1, offset=0)): if not (0 <= one_hz.index <= duration + 4): raise IndexError( "KPV '%s' index %.2f is not between 0 and %d" % (one_hz.name, one_hz.index, duration)) aligned_kpvs.append(one_hz) kpvs[param_name] = aligned_kpvs elif node.node_type is KeyTimeInstanceNode: params[param_name] = node aligned_ktis = [] for one_hz in node.get_aligned(P(frequency=1, offset=0)): if not (0 <= one_hz.index <= duration + 4): raise IndexError( "KTI '%s' index %.2f is not between 0 and %d" % (one_hz.name, one_hz.index, duration)) aligned_ktis.append(one_hz) ktis[param_name] = aligned_ktis elif node.node_type is FlightAttributeNode: params[param_name] = node try: # only has one Attribute node, store as a list for consistency flight_attrs[param_name] = [Attribute(node.name, node.value)] except: logger.warning( "Flight Attribute Node '%s' returned empty " "handed.", param_name) elif issubclass(node.node_type, SectionNode): aligned_section = node.get_aligned(P(frequency=1, offset=0)) for index, one_hz in enumerate(aligned_section): # SectionNodes allow slice starts and stops being None which # signifies the beginning and end of the data. To avoid # TypeErrors in subsequent derive methods which perform # arithmetic on section slice start and stops, replace with 0 # or hdf.duration. fallback = lambda x, y: x if x is not None else y duration = fallback(duration, 0) start = fallback(one_hz.slice.start, 0) stop = fallback(one_hz.slice.stop, duration) start_edge = fallback(one_hz.start_edge, 0) stop_edge = fallback(one_hz.stop_edge, duration) slice_ = slice(start, stop) one_hz = Section(one_hz.name, slice_, start_edge, stop_edge) aligned_section[index] = one_hz if not (0 <= start <= duration and 0 <= stop <= duration + 4): msg = "Section '%s' (%.2f, %.2f) not between 0 and %d" raise IndexError(msg % (one_hz.name, start, stop, duration)) if not 0 <= start_edge <= duration: msg = "Section '%s' start_edge (%.2f) not between 0 and %d" raise IndexError(msg % (one_hz.name, start_edge, duration)) if not 0 <= stop_edge <= duration + 4: msg = "Section '%s' stop_edge (%.2f) not between 0 and %d" raise IndexError(msg % (one_hz.name, stop_edge, duration)) #section_list.append(one_hz) params[param_name] = aligned_section sections[param_name] = list(aligned_section) elif issubclass(node.node_type, DerivedParameterNode): if duration: # check that the right number of nodes were returned Allow a # small tolerance. For example if duration in seconds is 2822, # then there will be an array length of 1411 at 0.5Hz and 706 # at 0.25Hz (rounded upwards). If we combine two 0.25Hz # parameters then we will have an array length of 1412. expected_length = duration * node.frequency if node.array is None or (force and len(node.array) == 0): logger.warning( "No array set; creating a fully masked " "array for %s", param_name) array_length = expected_length # Where a parameter is wholly masked, we fill the HDF # file with masked zeros to maintain structure. node.array = \ np_ma_masked_zeros(expected_length) else: array_length = len(node.array) length_diff = array_length - expected_length if length_diff == 0: pass elif 0 < length_diff < 5: logger.warning( "Cutting excess data for parameter '%s'. " "Expected length was '%s' while resulting " "array length was '%s'.", param_name, expected_length, len(node.array)) node.array = node.array[:expected_length] else: raise ValueError( "Array length mismatch for parameter " "'%s'. Expected '%s', resulting array " "length '%s'." % (param_name, expected_length, array_length)) hdf.set_param(node) # Keep hdf_keys up to date. node_mgr.hdf_keys.append(param_name) elif issubclass(node.node_type, ApproachNode): aligned_approach = node.get_aligned(P(frequency=1, offset=0)) for approach in aligned_approach: # Does not allow slice start or stops to be None. valid_turnoff = (not approach.turnoff or (0 <= approach.turnoff <= duration)) valid_slice = ((0 <= approach.slice.start <= duration) and (0 <= approach.slice.stop <= duration)) valid_gs_est = (not approach.gs_est or ((0 <= approach.gs_est.start <= duration) and (0 <= approach.gs_est.stop <= duration))) valid_loc_est = (not approach.loc_est or ((0 <= approach.loc_est.start <= duration) and (0 <= approach.loc_est.stop <= duration))) if not all( [valid_turnoff, valid_slice, valid_gs_est, valid_loc_est]): raise ValueError('ApproachItem contains index outside of ' 'flight data: %s' % approach) params[param_name] = aligned_approach approaches[param_name] = list(aligned_approach) else: raise NotImplementedError("Unknown Type %s" % node.__class__) continue return ktis, kpvs, sections, approaches, flight_attrs