Exemple #1
0
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")
Exemple #2
0
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()