def test_load_bad_aircraft_id(self): bad_name = 'qwertyuiop' bad_aircraft = aircraft.Aircraft(bad_name, '', '', 100.) with self.assertRaises(RuntimeError): self.sim = None self.sim = Simulation(aircraft=bad_aircraft)
def test_initialise_conditions_basic_config(self): plane = aircraft.f15 # manually reset JSBSim instance with new initial conditions if self.sim: self.sim.close() sim_frequency = 2 self.sim = Simulation(sim_frequency_hz=sim_frequency, aircraft=plane, init_conditions=None) self.assertEqual(self.sim.get_loaded_model_name(), plane.jsbsim_id, msg='JSBSim did not load expected aircraft model: ' + self.sim.get_loaded_model_name()) # check that properties are as we expected them to be expected_values = { prp.initial_u_fps: 328.0, prp.initial_v_fps: 0.0, prp.initial_w_fps: 0.0, prp.u_fps: 328.0, prp.v_fps: 0.0, prp.w_fps: 0.0, prp.BoundedProperty('simulation/dt', '', None, None): 1 / sim_frequency } for prop, expected in expected_values.items(): actual = self.sim[prop] self.assertAlmostEqual(expected, actual)
def reset(self): """ Resets the state of the environment and returns an initial observation. :return: array, the initial observation of the space. """ if self.sim: self.sim.close() self.sim = Simulation( aircraft_name=self.task.aircraft_name, init_conditions=self.task.init_conditions, jsbsim_freq=self.task.jsbsim_freq, agent_interaction_steps=self.task.agent_interaction_steps, ) self.state = self.get_observation() self.observation_space = self.task.get_observation_space() self.action_space = self.task.get_action_space() return self.state
def test_load_model(self): plane = aircraft.a320 self.sim = None self.sim = Simulation(aircraft=plane) actual_name = self.sim.get_loaded_model_name() self.assertEqual(plane.jsbsim_id, actual_name, msg=f'Unexpected aircraft model name after loading.')
def basic_task(): """ A simple task involving initing a JSBSimInstance to test multiprocessing. """ model = aircraft.cessna172P time.sleep(0.05) fdm = Simulation(aircraft=model) fdm.run() time.sleep(0.05) return 0
def _new_episode_init(self, sim: Simulation) -> None: """ This method is called at the start of every episode. It is used to set the value of any controls or environment properties not already defined in the task's initial conditions. By default it simply starts the aircraft engines. """ sim.start_engines() sim.raise_landing_gear() self._store_reward(RewardStub(1.0, 1.0), sim)
def task_step(self, sim: Simulation, action: Sequence[float], sim_steps: int) \ -> Tuple[NamedTuple, float, bool, Dict]: # input actions for prop, command in zip(self.action_variables, action): sim[prop] = command # run simulation for _ in range(sim_steps): sim.run() self._update_custom_properties(sim) state = self.State(*(sim[prop] for prop in self.state_variables)) done = self._is_terminal(sim) reward = self._get_reward(state, sim) if self.debug: self._validate_state(state, done, action, reward) self._store_reward(reward, sim) self.last_state = state info = {'reward': reward, 'action': action} return state, reward, done, info
def test_initialise_conditions_custom_config(self): """ Test JSBSimInstance initialisation with custom initial conditions. """ plane = aircraft.f15 init_conditions = { prp.initial_u_fps: 1000.0, prp.initial_v_fps: 0.0, prp.initial_w_fps: 1.0, prp.initial_altitude_ft: 5000, prp.initial_heading_deg: 12, prp.initial_r_radps: -0.1, } # map JSBSim initial condition properties to sim properties init_to_sim_conditions = { prp.initial_u_fps: prp.u_fps, prp.initial_v_fps: prp.v_fps, prp.initial_w_fps: prp.w_fps, prp.initial_altitude_ft: prp.altitude_sl_ft, prp.initial_heading_deg: prp.heading_deg, prp.initial_r_radps: prp.r_radps, } sim_frequency = 10 # manually reset JSBSim instance if self.sim: self.sim.close() self.sim = Simulation(sim_frequency, plane, init_conditions) # check JSBSim initial condition and simulation properties for init_prop, expected in init_conditions.items(): sim_prop = init_to_sim_conditions[init_prop] init_actual = self.sim[init_prop] sim_actual = self.sim[sim_prop] self.assertAlmostEqual(expected, init_actual, msg=f'wrong value for property {init_prop}') self.assertAlmostEqual(expected, sim_actual, msg=f'wrong value for property {sim_prop}') self.assertAlmostEqual(1.0 / sim_frequency, self.sim[prp.sim_dt])
def __init__(self, sim: Simulation, print_props: Tuple[prp.Property], block_until_loaded=True): """ Launches FlightGear in subprocess and starts figure for plotting actions. :param sim: Simulation that will be visualised :param aircraft: Aircraft to be loaded in FlightGear for visualisation :param print_props: collection of Propertys to be printed to Figure :param block_until_loaded: visualiser will block until it detects that FlightGear has loaded if True. """ self.configure_simulation_output(sim) self.print_props = print_props self.flightgear_process = self._launch_flightgear(sim.get_aircraft()) self.figure = FigureVisualiser(sim, print_props) if block_until_loaded: time.sleep(20)
def _new_episode_init(self, sim: Simulation) -> None: super()._new_episode_init(sim) sim.set_throttle_mixture_controls(self.THROTTLE_CMD, self.MIXTURE_CMD) sim[self.steps_left] = self.steps_left.max sim[self.target_track_deg] = self._get_target_track()
class TestSimulation(unittest.TestCase): sim: Simulation = None def setUp(self): if self.sim: self.sim.close() self.sim = Simulation() def tearDown(self): self.sim = None def test_init_jsbsim(self): self.assertIsInstance(self.sim.jsbsim, jsbsim.FGFDMExec, msg=f'Expected Simulation.jsbsim to hold an ' 'instance of JSBSim.') def test_load_model(self): plane = aircraft.a320 self.sim = None self.sim = Simulation(aircraft=plane) actual_name = self.sim.get_loaded_model_name() self.assertEqual(plane.jsbsim_id, actual_name, msg=f'Unexpected aircraft model name after loading.') def test_load_bad_aircraft_id(self): bad_name = 'qwertyuiop' bad_aircraft = aircraft.Aircraft(bad_name, '', '', 100.) with self.assertRaises(RuntimeError): self.sim = None self.sim = Simulation(aircraft=bad_aircraft) def test_get_property(self): self.setUp() # we expect certain values specified in the IC config XML file expected_values = { prp.initial_u_fps: 328.0, prp.initial_v_fps: 0.0, prp.initial_w_fps: 0.0, prp.u_fps: 328.0, prp.v_fps: 0.0, prp.w_fps: 0.0, } for prop, expected in expected_values.items(): actual = self.sim[prop] self.assertAlmostEqual(expected, actual) def test_get_bad_property(self): self.setUp() bad_prop = prp.BoundedProperty("bad_prop_name", "", 0, 0) with self.assertRaises(KeyError): _ = self.sim[bad_prop] def test_set_property(self): self.setUp() set_values = { prp.altitude_sl_ft: 1000, prp.aileron_cmd: 0.2, prp.elevator_cmd: 0.2, prp.rudder_cmd: 0.2, prp.throttle_cmd: 0.2, } for prop, value in set_values.items(): self.sim[prop] = value for prop, expected in set_values.items(): actual = self.sim[prop] self.assertAlmostEqual(expected, actual) def test_initialise_conditions_basic_config(self): plane = aircraft.f15 # manually reset JSBSim instance with new initial conditions if self.sim: self.sim.close() sim_frequency = 2 self.sim = Simulation(sim_frequency_hz=sim_frequency, aircraft=plane, init_conditions=None) self.assertEqual(self.sim.get_loaded_model_name(), plane.jsbsim_id, msg='JSBSim did not load expected aircraft model: ' + self.sim.get_loaded_model_name()) # check that properties are as we expected them to be expected_values = { prp.initial_u_fps: 328.0, prp.initial_v_fps: 0.0, prp.initial_w_fps: 0.0, prp.u_fps: 328.0, prp.v_fps: 0.0, prp.w_fps: 0.0, prp.BoundedProperty('simulation/dt', '', None, None): 1 / sim_frequency } for prop, expected in expected_values.items(): actual = self.sim[prop] self.assertAlmostEqual(expected, actual) def test_initialise_conditions_custom_config(self): """ Test JSBSimInstance initialisation with custom initial conditions. """ plane = aircraft.f15 init_conditions = { prp.initial_u_fps: 1000.0, prp.initial_v_fps: 0.0, prp.initial_w_fps: 1.0, prp.initial_altitude_ft: 5000, prp.initial_heading_deg: 12, prp.initial_r_radps: -0.1, } # map JSBSim initial condition properties to sim properties init_to_sim_conditions = { prp.initial_u_fps: prp.u_fps, prp.initial_v_fps: prp.v_fps, prp.initial_w_fps: prp.w_fps, prp.initial_altitude_ft: prp.altitude_sl_ft, prp.initial_heading_deg: prp.heading_deg, prp.initial_r_radps: prp.r_radps, } sim_frequency = 10 # manually reset JSBSim instance if self.sim: self.sim.close() self.sim = Simulation(sim_frequency, plane, init_conditions) # check JSBSim initial condition and simulation properties for init_prop, expected in init_conditions.items(): sim_prop = init_to_sim_conditions[init_prop] init_actual = self.sim[init_prop] sim_actual = self.sim[sim_prop] self.assertAlmostEqual(expected, init_actual, msg=f'wrong value for property {init_prop}') self.assertAlmostEqual(expected, sim_actual, msg=f'wrong value for property {sim_prop}') self.assertAlmostEqual(1.0 / sim_frequency, self.sim[prp.sim_dt]) def test_multiprocess_simulations(self): """ JSBSim segfaults when multiple instances are run on one process. Let's confirm that we can launch multiple processes each with 1 instance. """ processes = 4 with multiprocessing.Pool(processes) as pool: # N.B. basic_task is a top level function that inits JSBSim future_results = [pool.apply_async(basic_task) for _ in range(processes)] results = [f.get() for f in future_results] good_exit_code = 0 expected = [good_exit_code] * processes self.assertListEqual(results, expected, msg="multiprocess execution of JSBSim failed")
def setUp(self): if self.sim: self.sim.close() self.sim = Simulation()
class JSBSimEnv(gym.Env): """ A class wrapping the JSBSim flight dynamics module (FDM) for simulating aircraft as an RL environment conforming to the OpenAI Gym Env interface. An JsbSimEnv is instantiated with a Task that implements a specific aircraft control task with its own specific observation/action space and variables and agent_reward calculation. ATTRIBUTION: this class implements the OpenAI Gym Env API. Method docstrings have been adapted or copied from the OpenAI Gym source code. """ metadata = {'render.modes': ['human', 'csv']} def __init__(self, task): """ Constructor. Init some internal state, but JSBSimEnv.reset() must be called first before interacting with environment. :param task: the Task for the task agent is to perform """ self.sim = None self.task = task() self.observation_space = self.task.get_observation_space() # None self.action_space = self.task.get_action_space() # None self.state = None def step(self, action=None): """ Run one timestep of the environment's dynamics. When end of episode is reached, you are responsible for calling `reset()` to reset this environment's state. Accepts an action and returns a tuple (observation, reward, done, info). :param action: np.array, the agent's action, with same length as action variables. :return: state: agent's observation of the current environment reward: amount of reward returned after previous action done: whether the episode has ended, in which case further step() calls are undefined info: auxiliary information """ if action is not None: #print(action, self.action_space) #nb_action = 0 # for x in action: # nb_action += 1 # print(nb_action) # print(len(self.action_space.spaces)) if not len(action) == len(self.action_space.spaces): raise ValueError( 'mismatch between action and action space size') self.state = self.make_step(action) reward, done, info = self.task.get_reward( self.state, self.sim), self.is_terminal(), {} state = self.state if not done else self._get_clipped_state( ) # returned state should be in observation_space return state, reward, done, info def make_step(self, action=None): """ Calculates new state. :param action: array of floats, the agent's last action :return: observation: array, agent's observation of the environment state """ # take actions if action is not None: self.sim.set_property_values(self.task.get_action_var(), action) # run simulation self.sim.run() return self.get_observation() def reset(self): """ Resets the state of the environment and returns an initial observation. :return: array, the initial observation of the space. """ if self.sim: self.sim.close() self.sim = Simulation( aircraft_name=self.task.aircraft_name, init_conditions=self.task.init_conditions, jsbsim_freq=self.task.jsbsim_freq, agent_interaction_steps=self.task.agent_interaction_steps) self.state = self.get_observation() self.observation_space = self.task.get_observation_space() self.action_space = self.task.get_action_space() return self.state def is_terminal(self): """ Checks if the state is terminal. :return: bool """ is_not_contained = not self.observation_space.contains(self.state) return is_not_contained or self.task.is_terminal(self.state, self.sim) def render(self, mode='human', **kwargs): """Renders the environment. The set of supported modes varies per environment. (And some environments do not support rendering at all.) By convention, if mode is: - human: print on the terminal - csv: output to cvs files Note: Make sure that your class's metadata 'render.modes' key includes the list of supported modes. It's recommended to call super() in implementations to use the functionality of this method. :param mode: str, the mode to render with """ return self.task.render(self.sim, mode=mode, **kwargs) def seed(self, seed=None): """ Sets the seed for this env's random number generator(s). Note: Some environments use multiple pseudorandom number generators. We want to capture all such seeds used in order to ensure that there aren't accidental correlations between multiple generators. Returns: list<bigint>: Returns the list of seeds used in this env's random number generators. The first value in the list should be the "main" seed, or the value which a reproducer should pass to 'seed'. Often, the main seed equals the provided 'seed', but this won't be true if seed=None, for example. """ return def close(self): """ Cleans up this environment's objects Environments automatically close() when garbage collected or when the program exits. """ if self.sim: self.sim.close() def get_observation(self): """ get state observation from sim. :return: NamedTuple, the first state observation of the episode """ obs_list = self.sim.get_property_values( self.task.get_observation_var()) return tuple([np.array([obs]) for obs in obs_list]) def get_sim_time(self): """ Gets the simulation time from sim, a float. """ return self.sim.get_sim_time() def get_state(self): return self.sim.get_sim_state() def _get_clipped_state(self): clipped = [ np.clip(self.state[i], o.low, o.high) if self.task.state_var[i].clipped else self.state[i] for i, o in enumerate(self.observation_space) ] return tuple(clipped) def set_state(self, state): self.sim.set_sim_state(state) self.state = self.get_observation()
def _init_new_sim(self, dt, aircraft, initial_conditions): return Simulation(sim_frequency_hz=dt, aircraft=aircraft, init_conditions=initial_conditions)
def _init_new_sim(self, dt: float, aircraft: Aircraft, initial_conditions: Dict): return Simulation(sim_frequency_hz=dt, aircraft=aircraft, init_conditions=initial_conditions, allow_flightgear_output=False)
def configure_simulation_output(self, sim: Simulation): sim.enable_flightgear_output() sim.set_simulation_time_factor(self.FLIGHTGEAR_TIME_FACTOR)