def test_SpherePath_calculate_path_leg_distance(self): ecef_points = global_Point3d(ROUTE_LATS, ROUTE_LONS) ecef_path = SpherePath(ecef_points, TURN_DISTANCES) # Start point distance_0 = ecef_path.calculate_path_leg_distance(ecef_points[0], 0) self.assertEqual(distance_0, 0.0) # Point at end of a straight leg distance_1_0 = ecef_path.calculate_path_leg_distance(ecef_points[1], 0) assert_almost_equal(rad2nm(distance_1_0), EXPECTED_PATH_LENGTHS[1]) # Point at start of a straight leg distance_1_1 = ecef_path.calculate_path_leg_distance(ecef_points[1], 1) self.assertEqual(distance_0, 0.0) # Point in middle of a straight leg pos_1_5 = ecef_path.calculate_position(1, 0.5) distance_1_5 = ecef_path.calculate_path_leg_distance(pos_1_5, 1) assert_almost_equal(rad2nm(distance_1_5), 0.5 * EXPECTED_PATH_LENGTHS[2]) # Point toward end of a turning leg pos_1_9 = ecef_path.calculate_position(1, 0.99) distance_1_9 = ecef_path.calculate_path_leg_distance(pos_1_9, 1) assert_almost_equal(rad2nm(distance_1_9), 0.99 * EXPECTED_PATH_LENGTHS[2]) # Before start of a turning leg distance_2_9 = ecef_path.calculate_path_leg_distance(pos_1_9, 2) assert_almost_equal(rad2nm(distance_2_9), -0.57845277761762326) # Point toward start of a turning leg pos_2_1 = ecef_path.calculate_position(2, 0.01) distance_2_1 = ecef_path.calculate_path_leg_distance(pos_2_1, 2) assert_almost_equal(rad2nm(distance_2_1), 0.01 * EXPECTED_PATH_LENGTHS[3]) # Past the ned of a turning leg distance_1_2 = ecef_path.calculate_path_leg_distance(pos_2_1, 1) self.assertTrue(rad2nm(distance_1_2) > EXPECTED_PATH_LENGTHS[2]) assert_almost_equal(rad2nm(distance_1_2), 58.980918943627351) # Point along a turning leg pos_2_75 = ecef_path.calculate_position(2, 0.75) distance_2_75 = ecef_path.calculate_path_leg_distance(pos_2_75, 2) assert_almost_equal(rad2nm(distance_2_75), 0.75 * EXPECTED_PATH_LENGTHS[3]) # Last point distance_last = ecef_path.calculate_path_leg_distance( ecef_points[-1], 10) assert_almost_equal(rad2nm(distance_last), EXPECTED_PATH_LENGTHS[11]) # # A waypoint not on path distance_2_0 = ecef_path.calculate_path_leg_distance(ecef_points[2], 1) assert_almost_equal(rad2nm(distance_2_0), EXPECTED_PATH_LENGTHS[2], decimal=4)
def test_SpherefPath_init(self): ecef_points = global_Point3d(ROUTE_LATS, ROUTE_LONS) self.assertEqual(len(ecef_points), 12) ecef_path = SpherePath(ecef_points, TURN_DISTANCES) self.assertEqual(len(ecef_path.points), 12) # assert_array_almost_equal(ecef_path.points, ecef_points) self.assertEqual(len(ecef_path.turn_initiation_distances), 12) assert_array_almost_equal(ecef_path.turn_initiation_distances, TURN_DISTANCES) self.assertEqual(len(ecef_path.leg_lengths), 12) assert_array_almost_equal(rad2nm(ecef_path.leg_lengths), EXPECTED_LEG_LENGTHS) self.assertEqual(len(ecef_path.turn_angles), 12) assert_array_almost_equal(np.rad2deg(ecef_path.turn_angles), EXPECTED_TURN_ANGLES) self.assertEqual(len(ecef_path.path_lengths), 12) assert_array_almost_equal(rad2nm(ecef_path.path_lengths), EXPECTED_PATH_LENGTHS) lats, lons = ecef_path.point_lat_longs() assert_array_almost_equal(lats, ROUTE_LATS) assert_array_almost_equal(lons, ROUTE_LONS) assert_array_almost_equal(ecef_path.turn_initiation_distances_nm(), rad2nm(TURN_DISTANCES))
def test_calculate_path_cross_track_distance(self): ecef_points = global_Point3d(ROUTE_LATS, ROUTE_LONS) ecef_path = SpherePath(ecef_points, TURN_DISTANCES) assert_almost_equal( ecef_path.calculate_path_cross_track_distance(ecef_points[0], 0), 0.0) assert_almost_equal( ecef_path.calculate_path_cross_track_distance(ecef_points[1], 1), 0.0) xtd_2_1 = ecef_path.calculate_path_cross_track_distance( ecef_points[2], 1) assert_almost_equal(rad2nm(xtd_2_1), 4.1416795658323213) xtd_2_2 = ecef_path.calculate_path_cross_track_distance( ecef_points[2], 2) assert_almost_equal(rad2nm(xtd_2_2), 4.1416795658323213) xtd_3_2 = ecef_path.calculate_path_cross_track_distance( ecef_points[3], 2) assert_almost_equal(rad2nm(xtd_3_2), 8.2824069920496726) xtd_3_3 = ecef_path.calculate_path_cross_track_distance( ecef_points[3], 3) assert_almost_equal(rad2nm(xtd_3_3), 8.2824069920496726)
def test_SpherePath_section_distances_and_types(self): ecef_points = global_Point3d(ROUTE_LATS, ROUTE_LONS) ecef_path = SpherePath(ecef_points, TURN_DISTANCES) path_distances = rad2nm(ecef_path.path_distances()) distances, types = ecef_path.section_distances_and_types() self.assertEqual(len(distances), len(ecef_path) + 8) self.assertEqual(len(types), len(ecef_path) + 8) self.assertEqual(distances[0], 0.0) self.assertEqual(types[0], PointType.Waypoint) self.assertEqual(distances[1], path_distances[1]) self.assertEqual(types[1], PointType.Waypoint) self.assertTrue(distances[2] < path_distances[2]) self.assertEqual(types[2], PointType.TurnStart) self.assertTrue(distances[3] > path_distances[2]) self.assertEqual(types[3], PointType.TurnFinish) self.assertEqual(distances[-2], path_distances[-2]) self.assertEqual(types[-2], PointType.Waypoint) self.assertEqual(distances[-1], path_distances[-1]) self.assertEqual(types[-1], PointType.Waypoint)
def test_SpherePath_path_distances(self): ecef_points = global_Point3d(ROUTE_LATS, ROUTE_LONS) ecef_path = SpherePath(ecef_points, TURN_DISTANCES) path_distances = ecef_path.path_distances() self.assertEqual(len(path_distances), 12) assert_array_almost_equal(rad2nm(path_distances), EXPECTED_PATH_DISTANCES)
def test_EcefPath_path_distances(self): ecef_points = calculate_EcefPoints(ROUTE_LATS, ROUTE_LONS) ecef_path = EcefPath(ecef_points, TURN_DISTANCES) path_distances = ecef_path.path_distances() self.assertEqual(len(path_distances), 12) assert_array_almost_equal(rad2nm(path_distances), EXPECTED_PATH_DISTANCES)
def test_SpherePath_calculate_path_distances(self): ecef_points = global_Point3d(ROUTE_LATS, ROUTE_LONS) ecef_path = SpherePath(ecef_points, TURN_DISTANCES) distances = rad2nm( ecef_path.calculate_path_distances(ecef_points, ACROSS_TRACK_TOLERANCE)) self.assertEqual(len(distances), len(ecef_points)) self.assertEqual(distances[0], 0.0) assert_almost_equal(distances[11], EXPECTED_PATH_DISTANCES[11], decimal=6) assert_array_almost_equal(distances, EXPECTED_PATH_DISTANCES)
def test_EcefPath_calculate_cross_track_distances(self): ecef_points = calculate_EcefPoints(ROUTE_LATS, ROUTE_LONS) ecef_path = EcefPath(ecef_points, TURN_DISTANCES) distances = rad2nm( ecef_path.calculate_path_distances(ecef_points, ACROSS_TRACK_TOLERANCE)) self.assertEqual(len(distances), len(ecef_points)) xtds = ecef_path.calculate_cross_track_distances( ecef_points, distances) self.assertEqual(len(xtds), len(ecef_points)) assert_almost_equal(xtds[0], 0.0) assert_almost_equal(xtds[1], 0.0) assert_almost_equal(xtds[2], 4.1416795658323213) # 10NM TID assert_almost_equal(xtds[3], 8.2824069920496726) # 20NM TID assert_almost_equal(xtds[-1], 0.0)
def test_calculate_speeds(self): speeds = calculate_speeds(ELAPSED_TIMES, rad2nm(DISTANCES)) self.assertEqual(len(speeds), len(ELAPSED_TIMES)) assert_almost_equal(speeds[0], 334.99364794520545) assert_almost_equal(speeds[-1], 124.46279999998069)
def analyse_trajectory(flight_id, points_df, across_track_tolerance, method, N=DEFAULT_MOVING_MEDIAN_SAMPLES, M=DEFAULT_MOVING_AVERAGE_SAMPLES, max_duration=DEFAULT_SPEED_MAX_DURATION): """ Analyses and smooths positions in points_df. The function: - derives the horizontal path from the points_df latitudes and longitudes, - calculates the average time between positions - determines positions where the aircraft was cruising - classifys the trajectories vertical profile - derives and smooths the time profile, - derives and smooths the altitude profile, - constructs and retruns a SmoothedTrajectory containing the flight id, smoothed horizontal path, time profile and altitude profile. Parameters ---------- flight_id: string The id of the flight. points_df: a pandas DataFrame A DataFrame containing raw positions for a flight, sorted in time order. across_track_tolerance: float The maximum across track distance[Nautical Miles], default: 0.25 NM. method: string The smoothing method to use: 'mas', 'lm', 'trf' 'dogbox' N : integer The number of samples to consider for the speed moving median filter, default 5. M : integer The number of samples to consider for the speed moving average filter, default 5. max_duration: float The maximum time between points to smooth when calculating speed, default 120 [Seconds]. Returns ------- smoothed_trajectoy: SmoothedTrajectory The SmoothedTrajectory containing the flight id, smoothed horizontal path, time profile and altitude profile. metrics: list A list containing the flight id and trajectory quality metrics. """ # calculate the position period as seconds per point times = points_df['TIME'].values duration = calculate_delta_time(times[0], times[-1]) position_period = duration / (len(points_df) - 1) # convert across_track_tolerance to radians across_track_radians = np.deg2rad(across_track_tolerance / 60.0) ecef_points = global_Point3d(points_df['LAT'].values, points_df['LON'].values) # derive the EcefPath path = derive_horizontal_path(ecef_points, across_track_radians) horizontal_path_distances = rad2nm(path.path_distances()) # Ensure that the path is long enough if horizontal_path_distances[-1] < across_track_tolerance: raise ValueError("Path is short") lats, lons = path.point_lat_longs() tads = path.turn_initiation_distances_nm() hpath = HorizontalPath(lats, lons, tads) # Calculate distances of positions along the ECEF path in Nautical Miles path_distances = rad2nm( path.calculate_path_distances(ecef_points, across_track_radians)) # Raise an exception if the path_distances are shorter than the horizontal_path horizontal_path_length = horizontal_path_distances[ -1] - across_track_tolerance points_path_length = path_distances[-1] if points_path_length < horizontal_path_length: raise ValueError("Horizontal path distances are short") # Sort positions by path distance then time sorted_df = pd.DataFrame({ 'distance': path_distances, 'time': times, 'altitude': points_df['ALT'].values, 'points': ecef_points }) sorted_df.sort_values(by=['distance', 'time'], inplace=True) # determine whether the position order has changed sorted_path_distances = sorted_df['distance'].values unordered = (path_distances != sorted_path_distances).any() # find the indicies of starts and finishes of cruising sections and # classify the trajectory altitude profile altitudes = sorted_df['altitude'].values cruise_indicies = find_cruise_sections(altitudes) # calculate standard deviation and maximum across track error xtds = path.calculate_cross_track_distances(ecef_points, path_distances) xte_sd = xtds.std() max_xte, max_xte_index = find_most_extreme_value(xtds) max_xte = abs(max_xte) # Find duplicate positions, i.e. postions with across_track_tolerance of each other duplicate_positions = find_duplicate_values(sorted_path_distances, across_track_tolerance) # determine whether to smooth time with speed or scipy cuvre fit if method in CURVE_FIT_METHODS: timep, time_sd, max_time_diff, max_time_index = analyse_times( sorted_path_distances, sorted_df['time'].values, duplicate_positions, method) else: timep, time_sd, max_time_diff, max_time_index = analyse_speeds( sorted_path_distances, sorted_df['time'].values, duplicate_positions, N, M, max_duration) max_time_diff = abs(max_time_diff) altp, alt_sd, max_alt = analyse_altitudes(sorted_path_distances, altitudes, cruise_indicies) alt_profile_type = altp.type() # Calculate average periods in the climb, cruise and descent phases toc_distance = altp.top_of_climb_distance() tod_distance = altp.top_of_descent_distance() climb_period = timep.calculate_average_period(0.0, toc_distance) cruise_period = timep.calculate_average_period(toc_distance, tod_distance) descent_period = timep.calculate_average_period(tod_distance, timep.distances[-1]) return SmoothedTrajectory(flight_id, hpath, timep, altp), \ [flight_id, int(alt_profile_type), position_period, climb_period, cruise_period, descent_period, int(unordered), time_sd, max_time_diff, max_time_index, xte_sd, max_xte, max_xte_index, alt_sd, max_alt]
def test_SpherePath_calculate_path_distance(self): ecef_points = global_Point3d(ROUTE_LATS, ROUTE_LONS) ecef_path = SpherePath(ecef_points, TURN_DISTANCES) # Start point distance_0 = ecef_path.calculate_path_distance(ecef_points[0], 0, ACROSS_TRACK_TOLERANCE) self.assertEqual(distance_0, 0.0) # from next leg distance_0 = ecef_path.calculate_path_distance(ecef_points[0], 2, ACROSS_TRACK_TOLERANCE) self.assertEqual(distance_0, 0.0) # Point at end of first, straight leg distance_1_0 = ecef_path.calculate_path_distance( ecef_points[1], 0, ACROSS_TRACK_TOLERANCE) assert_almost_equal(rad2nm(distance_1_0), EXPECTED_PATH_DISTANCES[1]) # from next leg distance_1_0 = ecef_path.calculate_path_distance( ecef_points[1], 1, ACROSS_TRACK_TOLERANCE) assert_almost_equal(rad2nm(distance_1_0), EXPECTED_PATH_DISTANCES[1]) # from leg after next distance_1_0 = ecef_path.calculate_path_distance( ecef_points[1], 2, ACROSS_TRACK_TOLERANCE) assert_almost_equal(rad2nm(distance_1_0), EXPECTED_PATH_DISTANCES[1]) # Point in middle of first, straight leg pos_1_5 = ecef_path.calculate_position(1, 0.5) distance_1_5 = ecef_path.calculate_path_distance( pos_1_5, 1, ACROSS_TRACK_TOLERANCE) assert_almost_equal( rad2nm(distance_1_5), EXPECTED_PATH_DISTANCES[1] + 0.5 * EXPECTED_PATH_LENGTHS[2]) # from previous leg distance_1_5 = ecef_path.calculate_path_distance( pos_1_5, 0, ACROSS_TRACK_TOLERANCE) assert_almost_equal( rad2nm(distance_1_5), EXPECTED_PATH_DISTANCES[1] + 0.5 * EXPECTED_PATH_LENGTHS[2]) # from next leg distance_1_5 = ecef_path.calculate_path_distance( pos_1_5, 2, ACROSS_TRACK_TOLERANCE) assert_almost_equal( rad2nm(distance_1_5), EXPECTED_PATH_DISTANCES[1] + 0.5 * EXPECTED_PATH_LENGTHS[2]) # Point along a turning leg pos_2_75 = ecef_path.calculate_position(2, 0.75) distance_2_75 = ecef_path.calculate_path_distance( pos_2_75, 2, ACROSS_TRACK_TOLERANCE) assert_almost_equal( rad2nm(distance_2_75), EXPECTED_PATH_DISTANCES[2] + 0.75 * EXPECTED_PATH_LENGTHS[3]) # Last point distance_last = ecef_path.calculate_path_distance( ecef_points[-1], 10, ACROSS_TRACK_TOLERANCE) assert_almost_equal(rad2nm(distance_last), EXPECTED_PATH_DISTANCES[11], decimal=6) # from previous leg distance_last = ecef_path.calculate_path_distance( ecef_points[-1], 9, ACROSS_TRACK_TOLERANCE) assert_almost_equal(rad2nm(distance_last), EXPECTED_PATH_DISTANCES[11], decimal=6) # # A waypoint not on path distance_2_0 = ecef_path.calculate_path_distance( ecef_points[2], 1, ACROSS_TRACK_TOLERANCE) assert_almost_equal(rad2nm(distance_2_0), EXPECTED_PATH_DISTANCES[2], decimal=4)
def test_SphereTurnArc_init(self): """Test initialisation of SphereTurnArc class.""" ecef_point_0 = Point3d(ECEF_ICOSAHEDRON[1][0], ECEF_ICOSAHEDRON[1][1], ECEF_ICOSAHEDRON[1][2]) ecef_point_1 = Point3d(ECEF_ICOSAHEDRON[2][0], ECEF_ICOSAHEDRON[2][1], ECEF_ICOSAHEDRON[2][2]) ecef_point_2 = Point3d(ECEF_ICOSAHEDRON[3][0], ECEF_ICOSAHEDRON[3][1], ECEF_ICOSAHEDRON[3][2]) arc_0 = Arc3d(ecef_point_0, ecef_point_1) arc_1 = Arc3d(ecef_point_1, ecef_point_2) turn_0 = SphereTurnArc(arc_0, arc_1, TWENTY_NM) self.assertTrue(turn_0) # Ensure Turn start and end points are along inbound and outbound arcs assert_almost_equal(distance_radians(ecef_point_1, turn_0.start), TWENTY_NM) assert_almost_equal(distance_radians(ecef_point_1, turn_0.finish), TWENTY_NM) assert_almost_equal(arc_0.cross_track_distance(turn_0.start), 0.0) assert_almost_equal(arc_1.cross_track_distance(turn_0.finish), 0.0) ANGLE = 0.2 * np.pi RADIUS = 20.0 / np.tan(ANGLE / 2.0) # 61.553670693462841 NM assert_almost_equal(turn_0.angle, -ANGLE) assert_almost_equal(rad2nm(turn_0.radius), RADIUS) assert_almost_equal(rad2nm(turn_0.length()), RADIUS * ANGLE) assert_almost_equal(turn_0.radial_distance(turn_0.start), turn_0.radius, decimal=6) assert_almost_equal(turn_0.radial_distance(turn_0.finish), turn_0.radius, decimal=6) DISTANCE = TWENTY_NM / np.sin(ANGLE / 2.0) # 64.721359549995796 NM assert_almost_equal(turn_0.radial_distance(ecef_point_1), DISTANCE) assert_almost_equal(turn_0.cross_track_distance(turn_0.start), 0.0, decimal=6) assert_almost_equal(turn_0.cross_track_distance(turn_0.finish), 0.0, decimal=6) assert_almost_equal(turn_0.cross_track_distance(ecef_point_1), DISTANCE - turn_0.radius) self.assertEqual(turn_0.point_angle(turn_0.centre), 0.0) assert_almost_equal(turn_0.point_angle(turn_0.start), 0.0) assert_almost_equal(turn_0.point_angle(turn_0.finish), turn_0.angle, decimal=4) assert_almost_equal(turn_0.point_angle(ecef_point_1), turn_0.angle / 2.0, decimal=4) assert_almost_equal(turn_0.along_track_distance(turn_0.start), 0.0) assert_almost_equal(turn_0.along_track_distance(turn_0.finish), turn_0.length(), decimal=6) assert_almost_equal(turn_0.along_track_distance(ecef_point_1), turn_0.length() / 2.0, decimal=6) pos_1 = turn_0.position(turn_0.angle) assert_almost_equal(distance_radians(pos_1, turn_0.centre), turn_0.radius) assert_almost_equal(distance_radians(pos_1, turn_0.finish), 0.0, decimal=6) pos_2 = turn_0.position(turn_0.angle / 2.0) assert_almost_equal(distance_radians(pos_2, turn_0.centre), turn_0.radius) assert_almost_equal(turn_0.point_angle(pos_2), turn_0.angle / 2.0)