def generate_commands(program, drone_config_map: Dict[str, DroneConfig] = None, boundary_checker: BoundaryChecker = None, collision_checker: CollisionChecker = None, has_checks: bool = True, symbol_table: SymbolTable = None, function_table: FunctionTable = None): if drone_config_map is None: drone_config_map = {"DEFAULT": DefaultDroneConfig()} if boundary_checker is None: boundary_checker = BoundaryChecker(BoundaryConfig.no_limit()) if collision_checker is None: collision_checker = CollisionChecker(drone_config_map, DefaultCollisionConfig()) if symbol_table is None: symbol_table = SymbolTable() if function_table is None: function_table = FunctionTable() state_updater_map = { name: StateUpdater(config) for name, config in drone_config_map.items() } tree = _parse_program(program) drone_commands = Compiler(drone_config_map, symbol_table, function_table).visit(tree) if has_checks: boundary_checker.check(drone_commands, state_updater_map) collision_checker.check(drone_commands, state_updater_map) return drone_commands
def test_no_limit_values(self): boundary_config = BoundaryConfig.no_limit() self.assertEqual(float("inf"), boundary_config._max_seconds) self.assertEqual(float("inf"), boundary_config._max_x_meters) self.assertEqual(float("inf"), boundary_config._max_y_meters) self.assertEqual(float("inf"), boundary_config._max_z_meters) self.assertEqual(float("-inf"), boundary_config._min_x_meters) self.assertEqual(float("-inf"), boundary_config._min_y_meters) self.assertEqual(float("-inf"), boundary_config._min_z_meters)
def test_init_with_wrong_parameter_should_give_error(self): with self.assertRaises(ValueError) as context: BoundaryConfig(max_seconds=-1) self.assertTrue("max_seconds should >= 0" in str(context.exception)) with self.assertRaises(ValueError) as context: BoundaryConfig(max_x_meters=-1, min_x_meters=1) self.assertTrue( "max_x_meters should >= min_x_meters" in str(context.exception)) with self.assertRaises(ValueError) as context: BoundaryConfig(max_y_meters=-1, min_y_meters=1) self.assertTrue( "max_y_meters should >= min_y_meters" in str(context.exception)) with self.assertRaises(ValueError) as context: BoundaryConfig(max_z_meters=-1, min_z_meters=1) self.assertTrue( "max_z_meters should >= min_z_meters" in str(context.exception))
def test_if_given_boundary_checker_should_use_it_to_check_safety(self): commands = "main() {takeoff(); up(1000); land();}" with self.assertRaises(SafetyCheckError) as context: generate_commands(commands, boundary_checker=BoundaryChecker( BoundaryConfig(max_seconds=10000, max_z_meters=1))) self.assertTrue( "Drone 'DEFAULT': the z coordinate 1001 will go beyond its upper limit 1" in str(context.exception))
def test_str(self): boundary_config = BoundaryConfig(max_seconds=1, max_x_meters=2, max_y_meters=3, max_z_meters=4, min_x_meters=-2, min_y_meters=-3, min_z_meters=-4) self.assertEqual( "BoundaryConfig: { max_seconds: 1, x_range_meters: (-2, 2), " + "y_range_meters: (-3, 3), z_range_meters: (-4, 4) }", str(boundary_config))
def test_parse_if_not_provided_configs_should_use_default_configs(self): config = "{}" with self.assertLogs(logging.getLogger()) as log: actual_drone_configs, actual_boundary_config, acutal_collision_config = ConfigParser.parse(config) expected_log = ["WARNING:root:'drones' missing when parsing configs, using default drone_config. " + "Position estimation may be inaccurate.", "WARNING:root:'boundary_config' missing when parsing configs, using unlimited boundary_config. " + "Time and position will be unlimited.", "WARNING:root:'collision_config' missing when parsing configs, using default collision_config."] self.assertEqual(expected_log, log.output) self.assertEqual({"DEFAULT": DefaultDroneConfig()}, actual_drone_configs) self.assertEqual(BoundaryConfig.no_limit(), actual_boundary_config) self.assertEqual(DefaultCollisionConfig(), acutal_collision_config)
def test_if_given_state_updater_should_use_it_to_update_state(self): commands = "main() {takeoff(); land();}" with self.assertRaises(SafetyCheckError) as context: generate_commands(commands, drone_config_map={ "DEFAULT": DroneConfig(init_position=(0, 0, 0), speed_mps=1, rotate_speed_dps=90, takeoff_height_meters=10) }, boundary_checker=BoundaryChecker( BoundaryConfig(max_seconds=10, max_z_meters=1))) self.assertTrue( "Drone 'DEFAULT': the z coordinate 10 will go beyond its upper limit 1" in str(context.exception))
def test_parse_if_name_repeated_should_ignore_second_appearance(self): config = """ { "drones": [{ "name": "DRONE1", "init_position": {"x": 1, "y": 2, "z": 3}, "speed_mps": 2, "rotate_speed_dps": 180, "takeoff_height_meters": 2 },{ "name": "DRONE1", "init_position": {"x": 4, "y": 5, "z": 6}, "speed_mps": 1, "rotate_speed_dps": 90, "takeoff_height_meters": 1 }], "boundary_config": { "max_seconds": 100, "max_x_meters": 10, "max_y_meters": 20, "max_z_meters": 30, "min_x_meters": -10, "min_y_meters": -20, "min_z_meters": -30 }, "collision_config": { "collision_meters": 0.3, "time_interval_seconds": 0.5 } } """ with self.assertLogs(logging.getLogger()) as log: actual_drone_configs, actual_boundary_config, actual_collision_config = ConfigParser.parse(config) expected_log = ["WARNING:root:Drone name 'DRONE1' appeared more than ones in 'drones', ignored."] self.assertEqual(expected_log, log.output) expected_drone_configs = {"DRONE1": DroneConfig(init_position=(1, 2, 3), speed_mps=2, rotate_speed_dps=180, takeoff_height_meters=2)} expected_boundary_config = BoundaryConfig(max_seconds=100, max_x_meters=10, max_y_meters=20, max_z_meters=30, min_x_meters=-10, min_y_meters=-20, min_z_meters=-30) expected_collision_config = CollisionConfig(collision_meters=0.3, time_interval_seconds=0.5) self.assertEqual(expected_drone_configs, actual_drone_configs) self.assertEqual(expected_boundary_config, actual_boundary_config) self.assertEqual(expected_collision_config, actual_collision_config)
def test_parse_if_init_position_missing_fields_should_use_default_value(self): config = """ { "drones": [{ "name": "DRONE1", "init_position": {}, "speed_mps": 2, "rotate_speed_dps": 180, "takeoff_height_meters": 2 }], "boundary_config": { "max_seconds": 100, "max_x_meters": 10, "max_y_meters": 20, "max_z_meters": 30, "min_x_meters": -10, "min_y_meters": -20, "min_z_meters": -30 }, "collision_config": { "collision_meters": 0.3, "time_interval_seconds": 0.5 } } """ with self.assertLogs(logging.getLogger()) as log: actual_drone_configs, actual_boundary_config, actual_collision_config = ConfigParser.parse(config) expected_log = ["WARNING:root:'x' missing when parsing drone 'DRONE1', using default value 0. " + "Position estimation may be inaccurate.", "WARNING:root:'y' missing when parsing drone 'DRONE1', using default value 0. " + "Position estimation may be inaccurate.", "WARNING:root:'z' missing when parsing drone 'DRONE1', using default value 0. " + "Position estimation may be inaccurate."] self.assertEqual(expected_log, log.output) expected_drone_configs = {"DRONE1": DroneConfig(init_position=(0, 0, 0), speed_mps=2, rotate_speed_dps=180, takeoff_height_meters=2)} expected_boundary_config = BoundaryConfig(max_seconds=100, max_x_meters=10, max_y_meters=20, max_z_meters=30, min_x_meters=-10, min_y_meters=-20, min_z_meters=-30) expected_collision_config = CollisionConfig(collision_meters=0.3, time_interval_seconds=0.5) self.assertEqual(expected_drone_configs, actual_drone_configs) self.assertEqual(expected_boundary_config, actual_boundary_config) self.assertEqual(expected_collision_config, actual_collision_config)
def test_parse_if_configs_missing_fields_should_use_default_value(self): config = """ { "drones": [], "boundary_config": { }, "collision_config": { } } """ with self.assertLogs(logging.getLogger()) as log: actual_drone_configs, actual_boundary_config, actual_collision_config = ConfigParser.parse(config) expected_log = ["WARNING:root:'drones' is empty when parsing configs, using default drone_config. " + "Position estimation may be inaccurate.", "WARNING:root:'max_seconds' missing when parsing 'boundary_config', " + "using default value inf. There will be no limit on 'max_seconds'.", "WARNING:root:'max_x_meters' missing when parsing 'boundary_config', " + "using default value inf. There will be no limit on 'max_x_meters'.", "WARNING:root:'max_y_meters' missing when parsing 'boundary_config', " + "using default value inf. There will be no limit on 'max_y_meters'.", "WARNING:root:'max_z_meters' missing when parsing 'boundary_config', " + "using default value inf. There will be no limit on 'max_z_meters'.", "WARNING:root:'min_x_meters' missing when parsing 'boundary_config', " + "using default value -inf. There will be no limit on 'min_x_meters'.", "WARNING:root:'min_y_meters' missing when parsing 'boundary_config', " + "using default value -inf. There will be no limit on 'min_y_meters'.", "WARNING:root:'min_z_meters' missing when parsing 'boundary_config', " + "using default value -inf. There will be no limit on 'min_z_meters'.", "WARNING:root:'collision_meters' missing when parsing 'collision_config', " + "using default value 0. There will be no limit on 'collision_meters'.", "WARNING:root:'time_interval_seconds' missing when parsing 'collision_config', " + "using default value 0.1."] self.assertEqual(expected_log, log.output) expected_drone_configs = {"DEFAULT": DefaultDroneConfig()} expected_boundary_config = BoundaryConfig(max_seconds=float("inf"), max_x_meters=float("inf"), max_y_meters=float("inf"), max_z_meters=float("inf"), min_x_meters=float("-inf"), min_y_meters=float("-inf"), min_z_meters=float("-inf")) expected_collision_config = DefaultCollisionConfig() self.assertEqual(expected_drone_configs, actual_drone_configs) self.assertEqual(expected_boundary_config, actual_boundary_config) self.assertEqual(expected_collision_config, actual_collision_config)
def test_parse_if_provided_configs_should_parse_all_configs(self): config = """ { "drones": [{ "name": "DRONE1", "init_position": {"x": 1, "y": 2, "z": 3}, "speed_mps": 2, "rotate_speed_dps": 180, "takeoff_height_meters": 2 },{ "name": "DRONE2", "init_position": {"x": 4, "y": 5, "z": 6}, "speed_mps": 1, "rotate_speed_dps": 90, "takeoff_height_meters": 1 }], "boundary_config": { "max_seconds": 100, "max_x_meters": 10, "max_y_meters": 20, "max_z_meters": 30, "min_x_meters": -10, "min_y_meters": -20, "min_z_meters": -30 }, "collision_config": { "collision_meters": 0.3, "time_interval_seconds": 0.5 } } """ actual_drone_config, actual_boundary_config, acutal_collision_config = ConfigParser.parse(config) expected_drone_configs = {"DRONE1": DroneConfig(init_position=(1, 2, 3), speed_mps=2, rotate_speed_dps=180, takeoff_height_meters=2), "DRONE2": DroneConfig(init_position=(4, 5, 6), speed_mps=1, rotate_speed_dps=90, takeoff_height_meters=1)} expected_boundary_config = BoundaryConfig(max_seconds=100, max_x_meters=10, max_y_meters=20, max_z_meters=30, min_x_meters=-10, min_y_meters=-20, min_z_meters=-30) expected_collision_config = CollisionConfig(collision_meters=0.3, time_interval_seconds=0.5) self.assertEqual(expected_drone_configs, actual_drone_config) self.assertEqual(expected_boundary_config, actual_boundary_config) self.assertEqual(expected_collision_config, acutal_collision_config)
def setUp(self) -> None: self.boundary_config = BoundaryConfig(max_seconds=10, max_x_meters=5, max_y_meters=5, max_z_meters=5, min_x_meters=-5, min_y_meters=-5, min_z_meters=-5) self.state_updater_map = { "DRONE1": StateUpdater( DroneConfig(init_position=(0, 0, 0), speed_mps=1, rotate_speed_dps=90, takeoff_height_meters=1)), "DRONE2": StateUpdater( DroneConfig(init_position=(1, 0, 0), speed_mps=2, rotate_speed_dps=180, takeoff_height_meters=2)) }
def test_parse_if_drone_object_missing_fields_should_use_default_value(self): config = """ { "drones": [{}], "boundary_config": { "max_seconds": 100, "max_x_meters": 10, "max_y_meters": 20, "max_z_meters": 30, "min_x_meters": -10, "min_y_meters": -20, "min_z_meters": -30 }, "collision_config": { "collision_meters": 0.3, "time_interval_seconds": 0.5 } } """ with self.assertLogs(logging.getLogger()) as log: actual_drone_configs, actual_boundary_config, actual_collision_config = ConfigParser.parse(config) expected_log = ["WARNING:root:'name' missing when parsing object in 'drones', using default value 'DEFAULT'.", "WARNING:root:'init_position' missing when parsing drone 'DEFAULT', " + "using default value (0, 0, 0). Position estimation may be inaccurate.", "WARNING:root:'speed_mps' missing when parsing drone 'DEFAULT', " + "using default value 1. Position estimation may be inaccurate.", "WARNING:root:'rotate_speed_dps' missing when parsing drone 'DEFAULT', " + "using default value 90. Position estimation may be inaccurate.", "WARNING:root:'takeoff_height_meters' missing when parsing drone 'DEFAULT', " + "using default value 1. Position estimation may be inaccurate."] self.assertEqual(expected_log, log.output) expected_drone_configs = {"DEFAULT": DroneConfig(init_position=(0, 0, 0), speed_mps=1, rotate_speed_dps=90, takeoff_height_meters=1)} expected_boundary_config = BoundaryConfig(max_seconds=100, max_x_meters=10, max_y_meters=20, max_z_meters=30, min_x_meters=-10, min_y_meters=-20, min_z_meters=-30) expected_collision_config = CollisionConfig(collision_meters=0.3, time_interval_seconds=0.5) self.assertEqual(expected_drone_configs, actual_drone_configs) self.assertEqual(expected_boundary_config, actual_boundary_config) self.assertEqual(expected_collision_config, actual_collision_config)
def test_parse_if_name_is_empty_string_should_use_default_value(self): config = """ { "drones": [{ "name": "", "init_position": {"x": 1, "y": 2, "z": 3}, "speed_mps": 2, "rotate_speed_dps": 180, "takeoff_height_meters": 2 }], "boundary_config": { "max_seconds": 100, "max_x_meters": 10, "max_y_meters": 20, "max_z_meters": 30, "min_x_meters": -10, "min_y_meters": -20, "min_z_meters": -30 }, "collision_config": { "collision_meters": 0.3, "time_interval_seconds": 0.5 } } """ with self.assertLogs(logging.getLogger()) as log: actual_drone_configs, actual_boundary_config, actual_collision_config = ConfigParser.parse(config) expected_log = ["WARNING:root:'name' cannot be an empty string, using default value 'DEFAULT' instead."] self.assertEqual(expected_log, log.output) expected_drone_configs = {"DEFAULT": DroneConfig(init_position=(1, 2, 3), speed_mps=2, rotate_speed_dps=180, takeoff_height_meters=2)} expected_boundary_config = BoundaryConfig(max_seconds=100, max_x_meters=10, max_y_meters=20, max_z_meters=30, min_x_meters=-10, min_y_meters=-20, min_z_meters=-30) expected_collision_config = CollisionConfig(collision_meters=0.3, time_interval_seconds=0.5) self.assertEqual(expected_drone_configs, actual_drone_configs) self.assertEqual(expected_boundary_config, actual_boundary_config) self.assertEqual(expected_collision_config, actual_collision_config)
def test_if_not_given_state_updater_should_use_default_to_update_state( self): commands = "main() {takeoff(); land();}" generate_commands(commands, boundary_checker=BoundaryChecker( BoundaryConfig(max_seconds=10, max_z_meters=1)))
def test_check_state_if_beyond_limit_should_give_error(self): boundary_config = BoundaryConfig(max_seconds=0, max_x_meters=0, max_y_meters=0, max_z_meters=0, min_x_meters=0, min_y_meters=0, min_z_meters=0) with self.assertRaises(SafetyCheckError) as context: boundary_config.check_state('default', State(x_meters=10)) self.assertTrue( "Drone 'default': the x coordinate 10 will go beyond its upper limit 0" in str(context.exception)) with self.assertRaises(SafetyCheckError) as context: boundary_config.check_state('default', State(y_meters=10)) self.assertTrue( "Drone 'default': the y coordinate 10 will go beyond its upper limit 0" in str(context.exception)) with self.assertRaises(SafetyCheckError) as context: boundary_config.check_state('default', State(z_meters=10)) self.assertTrue( "Drone 'default': the z coordinate 10 will go beyond its upper limit 0" in str(context.exception)) with self.assertRaises(SafetyCheckError) as context: boundary_config.check_state('default', State(x_meters=-10)) self.assertTrue( "Drone 'default': the x coordinate -10 will go beyond its lower limit 0" in str(context.exception)) with self.assertRaises(SafetyCheckError) as context: boundary_config.check_state('default', State(y_meters=-10)) self.assertTrue( "Drone 'default': the y coordinate -10 will go beyond its lower limit 0" in str(context.exception)) with self.assertRaises(SafetyCheckError) as context: boundary_config.check_state('default', State(z_meters=-10)) self.assertTrue( "Drone 'default': the z coordinate -10 will go beyond its lower limit 0" in str(context.exception)) with self.assertRaises(SafetyCheckError) as context: boundary_config.check_state('default', State(time_used_seconds=10)) self.assertTrue( "Drone 'default': the time used 10 seconds will go beyond the time limit 0 seconds" in str(context.exception))
def test_eq(self): self.assertEqual(BoundaryConfig(), BoundaryConfig()) self.assertNotEqual(BoundaryConfig(max_seconds=0), BoundaryConfig(max_seconds=1)) self.assertNotEqual(BoundaryConfig(max_x_meters=0), BoundaryConfig(max_x_meters=1)) self.assertNotEqual(BoundaryConfig(max_y_meters=0), BoundaryConfig(max_y_meters=1)) self.assertNotEqual(BoundaryConfig(max_z_meters=0), BoundaryConfig(max_z_meters=1)) self.assertNotEqual(BoundaryConfig(min_x_meters=0), BoundaryConfig(min_x_meters=-1)) self.assertNotEqual(BoundaryConfig(min_y_meters=0), BoundaryConfig(min_y_meters=-1)) self.assertNotEqual(BoundaryConfig(min_z_meters=0), BoundaryConfig(min_z_meters=-1)) self.assertNotEqual(None, BoundaryConfig())
def _parse_boundary_config(data: dict) -> BoundaryConfig: if "boundary_config" in data: if "max_seconds" in data["boundary_config"]: max_seconds = data["boundary_config"]["max_seconds"] else: warning( "'max_seconds' missing when parsing 'boundary_config', using default value inf. " + "There will be no limit on 'max_seconds'.") max_seconds = float('inf') if "max_x_meters" in data["boundary_config"]: max_x_meters = data["boundary_config"]["max_x_meters"] else: warning( "'max_x_meters' missing when parsing 'boundary_config', using default value inf. " + "There will be no limit on 'max_x_meters'.") max_x_meters = float('inf') if "max_y_meters" in data["boundary_config"]: max_y_meters = data["boundary_config"]["max_y_meters"] else: warning( "'max_y_meters' missing when parsing 'boundary_config', using default value inf. " + "There will be no limit on 'max_y_meters'.") max_y_meters = float('inf') if "max_z_meters" in data["boundary_config"]: max_z_meters = data["boundary_config"]["max_z_meters"] else: warning( "'max_z_meters' missing when parsing 'boundary_config', using default value inf. " + "There will be no limit on 'max_z_meters'.") max_z_meters = float('inf') if "min_x_meters" in data["boundary_config"]: min_x_meters = data["boundary_config"]["min_x_meters"] else: warning( "'min_x_meters' missing when parsing 'boundary_config', using default value -inf. " + "There will be no limit on 'min_x_meters'.") min_x_meters = float('-inf') if "min_y_meters" in data["boundary_config"]: min_y_meters = data["boundary_config"]["min_y_meters"] else: warning( "'min_y_meters' missing when parsing 'boundary_config', using default value -inf. " + "There will be no limit on 'min_y_meters'.") min_y_meters = float('-inf') if "min_z_meters" in data["boundary_config"]: min_z_meters = data["boundary_config"]["min_z_meters"] else: warning( "'min_z_meters' missing when parsing 'boundary_config', using default value -inf. " + "There will be no limit on 'min_z_meters'.") min_z_meters = float('-inf') boundary_config = BoundaryConfig(max_seconds, max_x_meters, max_y_meters, max_z_meters, min_x_meters, min_y_meters, min_z_meters) else: warning( "'boundary_config' missing when parsing configs, using unlimited boundary_config. " + "Time and position will be unlimited.") boundary_config = BoundaryConfig.no_limit() return boundary_config
def test_check_state_if_no_limit_should_not_give_error(self): boundary_config = BoundaryConfig() for number in [1e-5, 1e-3, 1e0, 1e3, 1e5, 1e10, float('inf')]: boundary_config.check_state('default', State(x_meters=number)) boundary_config.check_state('default', State(y_meters=number)) boundary_config.check_state('default', State(z_meters=number)) boundary_config.check_state('default', State(x_meters=-number)) boundary_config.check_state('default', State(y_meters=-number)) boundary_config.check_state('default', State(z_meters=-number)) boundary_config.check_state('default', State(time_used_seconds=number))