def test_pendulum_integration(self):
        """
        @brief   Compare pendulum motion, as simulated by Jiminy, against an
                 equivalent simulation done in python.

        @details Since we don't have a simple analytical expression for the
                 solution of a (nonlinear) pendulum motion, we perform the
                 simulation in Python, with the same integrator, and compare
                 both results.
        """
        # Create an engine: no controller and no internal dynamics
        engine = jiminy.Engine()
        setup_controller_and_engine(engine, self.robot)

        # Run simulation and extract log data
        x0 = np.array([0.1, 0.0])
        tf = 2.0
        time, x_jiminy = simulate_and_get_state_evolution(engine,
                                                          tf,
                                                          x0,
                                                          split=False)

        # Pendulum dynamics
        def dynamics(t, x):
            return np.array([x[1], self.g / self.l * np.sin(x[0])])

        # Integrate this non-linear dynamics
        x_rk_python = integrate_dynamics(time, x0, dynamics)

        # Compare the numerical and numerical integration of analytical model
        # using Scipy
        self.assertTrue(np.allclose(x_jiminy, x_rk_python, atol=TOLERANCE))
    def _get_simulated_and_analytical_solutions(self, engine, tf, xInit):
        """
        @brief   Simulate the system dynamics and compute the corresponding
                 analytical solution at the same timesteps.
        """
        # Run simulation and extract some information from log data
        time, x_jiminy = simulate_and_get_state_evolution(
            engine, tf, xInit, split=False)

        # Compute analytical solution
        x_analytical = np.stack([
            scipy.linalg.expm(self.A * t).dot(self.x0)
            for t in time], axis=0)

        return time, x_jiminy, x_analytical
    def test_armature(self):
        """
        @brief Verify the dynamics of the system when adding  rotor inertia.
        """
        # Configure the robot: set rotor inertia
        J = 0.1
        motor_options = self.robot.get_motors_options()
        motor_options["PendulumJoint"]['enableArmature'] = True
        motor_options["PendulumJoint"]['armature'] = J
        self.robot.set_motors_options(motor_options)

        # Dynamics: simulate a spring of stiffness k
        k_spring = 500

        def spring_force(t, q, v, sensors_data, u_custom):
            u_custom[:] = -k_spring * q.flatten()

        # Initialize the controller and setup the engine
        engine = jiminy.Engine()
        setup_controller_and_engine(engine,
                                    self.robot,
                                    internal_dynamics=spring_force)

        # Configure the engine
        engine_options = engine.get_options()
        engine_options["stepper"]["solver"] = "runge_kutta_dopri5"
        engine_options["stepper"]["tolAbs"] = TOLERANCE * 1e-1
        engine_options["stepper"]["tolRel"] = TOLERANCE * 1e-1
        engine_options["world"]["gravity"] = np.zeros(6)
        engine.set_options(engine_options)

        # Run simulation and extract log data
        x0 = np.array([0.1, 0.0])
        tf = 2.0
        time, x_jiminy = simulate_and_get_state_evolution(engine,
                                                          tf,
                                                          x0,
                                                          split=False)

        # Analytical solution: a simple mass on a spring
        I_eq = self.I + J
        A = np.array([[0, 1], [-k_spring / I_eq, 0]])
        x_analytical = np.stack(
            [scipy.linalg.expm(A * t).dot(x0) for t in time], axis=0)

        self.assertTrue(np.allclose(x_jiminy, x_analytical, atol=TOLERANCE))
示例#4
0
    def test_multi_robot(self):
        """
        @brief   Simulate interaction between two robots.

        @details The system simulated can be represented as such: each system
                 is a single mass linked to a spring, and a spring k_12 links
                 both systems.

                    k1     M1   k(1->2)  M2
                //| <><>  |__| <><><><> |  |
                             k2         |  |
                //| <><> <><> <><> <><> |__|
        """
        # Specify model
        urdf_name = "linear_single_mass.urdf"
        motors_names = ["Joint"]

        # Specify spring stiffness and damping for this simulation
        # First two parameters are the stiffness of each system,
        # third is the stiffness of the coupling spring
        k = np.array([100, 20, 50])
        nu = np.array([0.1, 0.2, 0.2])

        # Create controllers
        class Controllers:
            def __init__(self, k, nu):
                self.k = k
                self.nu = nu

            def compute_command(self, t, q, v, sensors_data, command):
                pass

            def internal_dynamics(self, t, q, v, sensors_data, u_custom):
                u_custom[:] = - self.k * q - self.nu * v

        # Create two identical robots
        engine = jiminy.EngineMultiRobot()

        # Configure the engine
        engine_options = engine.get_options()
        engine_options["stepper"]["solver"] = "runge_kutta_dopri5"
        engine_options["stepper"]["tolAbs"] = TOLERANCE * 1e-1
        engine_options["stepper"]["tolRel"] = TOLERANCE * 1e-2
        engine.set_options(engine_options)

        system_names = ['FirstSystem', 'SecondSystem']
        robots = []
        for i in range(2):
            robots.append(load_urdf_default(urdf_name, motors_names))

            # Create controller
            controller = Controllers(k[i], nu[i])

            controller = jiminy.BaseControllerFunctor(
                controller.compute_command, controller.internal_dynamics)
            controller.initialize(robots[i])

            # Add system to engine.
            engine.add_system(system_names[i], robots[i], controller)

        # Add coupling force between both systems: a spring between both masses
        def force(t, q1, v1, q2, v2, f):
            f[0] = k[2] * (q2[0] - q1[0]) + nu[2] * (v2[0] - v1[0])

        engine.register_force_coupling(
            system_names[0], system_names[1], "Mass", "Mass", force)

        # Run simulation and extract some information from log data
        x0 = {'FirstSystem': np.array([0.1, 0.0]),
              'SecondSystem': np.array([-0.1, 0.0])}
        tf = 10.0
        time, x_jiminy = simulate_and_get_state_evolution(
            engine, tf, x0, split=False)
        x_jiminy = np.concatenate(x_jiminy, axis=-1)

        # Define analytical system dynamics: two masses linked by three springs
        m = [r.pinocchio_model_th.inertias[1].mass for r in robots]
        k_eq = [x + k[2] for x in k]
        nu_eq = [x + nu[2] for x in nu]
        A = np.array([[            0.0,              1.0,             0.0,              0.0],
                      [-k_eq[0] / m[0], -nu_eq[0] / m[0],     k[2] / m[0],      nu[2] / m[0]],
                      [            0.0,              0.0,             0.0,              1.0],
                      [    k[2] / m[1],     nu[2] / m[1], -k_eq [1]/ m[1], -nu_eq[1] / m[1]]])

        # Compute analytical solution
        x_analytical = np.stack([
            expm(A * t).dot(x_jiminy[0, :]) for t in time], axis=0)

        # Compare the numerical and analytical solutions
        self.assertTrue(np.allclose(x_jiminy, x_analytical, atol=TOLERANCE))
示例#5
0
    def _test_friction_model(self, shape):
        """
        @brief    Validate the friction model.

        @details  The transition between dry, dry-viscous, and viscous friction
                  is assessed. The energy variation and the steady state are
                  also compared to the theoretical model.
        """
        # Create the robot and engine
        robot, weight, height, *_ = self._setup(shape)

        # Create, initialize, and configure the engine
        engine = jiminy.Engine()
        self._setup_controller_and_engine(engine, robot)

        # Set some extra options of the engine
        engine_options = engine.get_options()
        engine_options['contacts']['transitionEps'] = 1.0e-6
        engine_options['contacts']['friction'] = self.friction
        engine_options['contacts']['transitionVelocity'] = self.transtion_vel
        engine_options["stepper"]["controllerUpdatePeriod"] = self.dtMax
        engine.set_options(engine_options)

        # Register an impulse of force
        t0, dt, Fx = 0.05, 0.8, 5.0
        F = np.array([Fx, 0.0, 0.0, 0.0, 0.0, 0.0])
        engine.register_force_impulse(self.body_name, t0, dt, F)

        # Run simulation
        x0 = neutral_state(robot, split=False)
        x0[2] = height
        tf = 1.5
        time, _, v_jiminy = simulate_and_get_state_evolution(
            engine, tf, x0, split=True)
        v_x_jiminy = v_jiminy[:, 0]

        # Validate the stiction model: check the transition between dry and
        # viscous friction because of stiction phenomena.
        log_data, _ = engine.get_log()
        acceleration = log_data[
            'HighLevelController.currentFreeflyerAccelerationLinX']
        jerk = np.diff(acceleration) / np.diff(time)
        snap =  np.diff(jerk) / np.diff(time[1:])
        snap_rel = np.abs(snap / np.max(snap))
        snap_disc = time[1:-1][snap_rel > 1.0e-5]
        snap_disc = snap_disc[np.concatenate((
            [False], np.diff(snap_disc) > 2 * self.dtMax))]

        snap_disc_analytical_dry = time[(
            (v_x_jiminy > (self.transtion_vel - 2.0e-5)) &
            (v_x_jiminy < (self.transtion_vel + 2.0e-5)))]
        snap_disc_analytical = np.sort(np.concatenate(
            (snap_disc_analytical_dry,
             np.array([t0, t0 + self.dtMax, t0 + dt, t0 + dt + self.dtMax]))))
        snap_disc_analytical = snap_disc_analytical[np.concatenate((
            [False], np.diff(snap_disc_analytical) > 2 * self.dtMax))]

        self.assertTrue(len(snap_disc) == len(snap_disc_analytical))
        self.assertTrue(np.allclose(
            snap_disc, snap_disc_analytical, atol=2*self.dtMax))

        # Check that the energy increases only when the force is applied
        tolerance_E = 1e-9

        E_robot = log_data['HighLevelController.energy']
        E_diff_robot = np.concatenate((
            np.diff(E_robot) / np.diff(time),
            np.zeros((1,), dtype=E_robot.dtype)))
        E_inc_range = time[np.where(E_diff_robot > tolerance_E)[0][[0, -1]]]
        E_inc_range_analytical = np.array([t0, t0 + dt - self.dtMax])

        self.assertTrue(np.allclose(
            E_inc_range, E_inc_range_analytical, atol=tolerance_E))

        # Check that the steady state matches the theory.
        # Note that a specific tolerance is used for the acceleration since the
        # steady state is not perfectly reached.
        tolerance_acc = 1e-6

        v_steady = v_x_jiminy[np.isclose(time, t0 + dt)]
        v_steady_analytical = Fx / (self.friction * weight)
        a_steady = acceleration[
            (time > t0 + dt - self.dtMax) & (time < t0 + dt + self.dtMax)]

        self.assertTrue(len(a_steady) == 1)
        self.assertTrue(a_steady < tolerance_acc)
        self.assertTrue(np.allclose(
            v_steady, v_steady_analytical, atol=TOLERANCE))
示例#6
0
    def _test_collision_and_contact_dynamics(self, shape):
        """
        @brief    Validate the collision body and contact point dynamics.

        @details  The energy is expected to decrease slowly when penetrating
                  into the ground, but should stay constant otherwise. Then,
                  the equilibrium point must also match the physics. Note that
                  the friction model is not assessed here.
        """
        # Create the robot
        robot, weight, height, joint_idx, _ = self._setup(shape)

        # Create, initialize, and configure the engine
        engine = jiminy.Engine()
        self._setup_controller_and_engine(engine, robot)

        # Set some extra options of the engine, to avoid assertion failure
        # because of problem regularization and outliers
        engine_options = engine.get_options()
        engine_options['contacts']['transitionEps'] = 1.0e-6
        engine_options["stepper"]["controllerUpdatePeriod"] = self.dtMax
        engine.set_options(engine_options)

        # Run simulation and extract some information from log data
        x0 = neutral_state(robot, split=False)
        x0[2] = 1.0
        tf = 1.5
        _, x_jiminy = simulate_and_get_state_evolution(
            engine, tf, x0, split=False)
        q_z_jiminy = x_jiminy[:, 2]
        v_z_jiminy = x_jiminy[:, 9]
        penetration_depth = np.minimum(q_z_jiminy - height, 0.0)

        # Total energy and derivative
        log_data, _ = engine.get_log()
        E_robot = log_data['HighLevelController.energy']
        E_contact = 1 / 2 * self.k_contact * penetration_depth ** 2
        E_tot = E_robot + E_contact
        E_diff_robot = np.concatenate((
            np.diff(E_robot) / self.dtMax,
            np.zeros((1,), dtype=E_robot.dtype)))
        E_diff_tot = savgol_filter(E_tot, 201, 2, deriv=1, delta=self.dtMax)

        # Check that the total energy never increases.
        # One must use a specific, less restrictive, tolerance, because of
        # numerical differentiation and filtering error.
        self.assertTrue(np.all(E_diff_tot < 1.0e-3))

        # Check that the energy of robot only increases when the robot is
        # moving upward while still in the ground. This is done by check
        # that there is not two consecutive samples violating this law.
        # Note that the energy must be non-zero to this test to make
        # sense, otherwise the integration error and log accuracy makes
        # the test fail.
        tolerance_depth = 1e-9
        self.assertTrue(np.all(np.diff(np.where(
            (abs(E_diff_robot) > tolerance_depth) & ((E_diff_robot > 0.0) != \
                ((v_z_jiminy > 0.0) & (penetration_depth < 0.0))))[0]) > 1))

        # Compare the numerical and analytical equilibrium state.
        f_ext_z = engine.system_state.f_external[joint_idx].linear[2]
        self.assertTrue(np.allclose(f_ext_z, weight, atol=TOLERANCE))
        self.assertTrue(np.allclose(
            -penetration_depth[-1], weight / self.k_contact, atol=TOLERANCE))
    def test_fixed_body_constraint_armature(self):
        """
        @brief Test fixed body constraint together with rotor inertia.
        """
        # Create robot with freeflyer, set rotor inertia.
        robot = load_urdf_default(self.urdf_name,
                                  self.motors_names,
                                  has_freeflyer=True)

        # Enable rotor inertia
        J = 0.1
        motor_options = robot.get_motors_options()
        motor_options["PendulumJoint"]['enableArmature'] = True
        motor_options["PendulumJoint"]['armature'] = J
        robot.set_motors_options(motor_options)

        # Set fixed body constraint.
        freeflyer_constraint = jiminy.FixedFrameConstraint("world")
        robot.add_constraint("world", freeflyer_constraint)

        # Create an engine: simulate a spring internal dynamics
        k_spring = 500

        def spring_force(t, q, v, sensors_data, u_custom):
            u_custom[:] = -k_spring * q[-1]

        engine = jiminy.Engine()
        setup_controller_and_engine(engine,
                                    robot,
                                    internal_dynamics=spring_force)

        # Configure the engine
        engine_options = engine.get_options()
        engine_options["stepper"]["solver"] = "runge_kutta_dopri5"
        engine_options["stepper"]["tolAbs"] = TOLERANCE * 1e-1
        engine_options["stepper"]["tolRel"] = TOLERANCE * 1e-1
        engine_options["world"]["gravity"] = np.zeros(6)
        engine.set_options(engine_options)

        # Run simulation and extract some information from log data
        x0 = np.array([0.1, 0.0])
        qInit, vInit = neutral_state(robot, split=True)
        qInit[-1], vInit[-1] = x0
        xInit = np.concatenate((qInit, vInit))
        tf = 2.0
        time, q_jiminy, v_jiminy = simulate_and_get_state_evolution(engine,
                                                                    tf,
                                                                    xInit,
                                                                    split=True)

        # Analytical solution: dynamics should be unmodified by
        # the constraint, so we have a simple mass on a spring.
        I_eq = self.I + J
        A = np.array([[0, 1], [-k_spring / I_eq, 0]])
        x_analytical = np.stack(
            [scipy.linalg.expm(A * t).dot(x0) for t in time], axis=0)

        self.assertTrue(
            np.allclose(q_jiminy[:, :-1], qInit[:-1], atol=TOLERANCE))
        self.assertTrue(
            np.allclose(v_jiminy[:, :-1], vInit[:-1], atol=TOLERANCE))
        self.assertTrue(
            np.allclose(q_jiminy[:, -1], x_analytical[:, 0], atol=TOLERANCE))
        self.assertTrue(
            np.allclose(v_jiminy[:, -1], x_analytical[:, 1], atol=TOLERANCE))
    def test_flexibility_armature(self):
        """
        @brief Test the addition of a flexibility in the system.

        @details This test asserts that, by adding a flexibility and a rotor
                 inertia, the output is 'sufficiently close' to a SEA system:
                 see 'note_on_flexibilty_model.pdf' for more information as to
                 why this is not a true equality.
        """
        # Physical parameters: rotor inertia, spring stiffness and damping.
        J = 0.1
        k = 20.0
        nu = 0.1

        # Enable flexibility
        model_options = self.robot.get_model_options()
        model_options["dynamics"]["enableFlexibleModel"] = True
        model_options["dynamics"]["flexibilityConfig"] = [{
            'frameName':
            "PendulumJoint",
            'stiffness':
            k * np.ones(3),
            'damping':
            nu * np.ones(3)
        }]
        self.robot.set_model_options(model_options)

        # Enable rotor inertia
        motor_options = self.robot.get_motors_options()
        motor_options["PendulumJoint"]['enableArmature'] = True
        motor_options["PendulumJoint"]['armature'] = J
        self.robot.set_motors_options(motor_options)

        # Create an engine: PD controller on motor and no internal dynamics
        k_control, nu_control = 100.0, 1.0

        def ControllerPD(t, q, v, sensors_data, command):
            command[:] = -k_control * q[4] - nu_control * v[3]

        engine = jiminy.Engine()
        setup_controller_and_engine(engine,
                                    self.robot,
                                    compute_command=ControllerPD)

        # Configure the engine
        engine_options = engine.get_options()
        engine_options["stepper"]["solver"] = "runge_kutta_dopri5"
        engine_options["stepper"]["tolAbs"] = TOLERANCE * 1e-1
        engine_options["stepper"]["tolRel"] = TOLERANCE * 1e-1
        engine_options["world"]["gravity"] = np.zeros(6)
        engine.set_options(engine_options)

        # Run simulation and extract some information from log data.
        # Note that to avoid having to handle angle conversion, start with an
        # initial velocity for the output mass.
        v_init = 0.1
        x0 = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, v_init, 0.0, 0.0])
        tf = 10.0
        time, x_jiminy = simulate_and_get_state_evolution(engine,
                                                          tf,
                                                          x0,
                                                          split=False)

        # Convert quaternion to RPY
        x_jiminy = np.stack([
            np.concatenate((
                matrixToRpy(Quaternion(x[:4][:, np.newaxis]).matrix())\
                    .astype(x.dtype, copy=False),
                x[4:]
            )) for x in x_jiminy
        ], axis=0)

        # First, check that there was no motion other than along the Y axis.
        self.assertTrue(np.allclose(x_jiminy[:, [0, 2, 4, 6]], 0))

        # Now let's group x_jiminy to match the analytical system:
        # flexibility angle, pendulum angle, flexibility velocity, pendulum
        # velocity.
        x_jiminy_extract = x_jiminy[:, [1, 3, 5, 7]]

        # Simulate the system: a perfect SEA system.
        A = np.array([[0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0],
                      [
                          -k * (1 / self.I + 1 / J), k_control / J,
                          -nu * (1 / self.I + 1 / J), nu_control / J
                      ], [k / J, -k_control / J, nu / J, -nu_control / J]])
        x_analytical = np.stack(
            [scipy.linalg.expm(A * t).dot(x_jiminy_extract[0]) for t in time],
            axis=0)

        # This test has a specific tolerance because we know the dynamics don't exactly
        # match: they are however very close, since the inertia of the flexible element
        # is negligible before I.
        self.assertTrue(np.allclose(x_jiminy_extract, x_analytical, atol=1e-4))
    def test_pendulum_force_impulse(self):
        """
        @brief   Validate the impulse-momentum theorem

        @details The analytical expression for the solution is exact for
                 impulse of force that are perfect dirac functions.
        """
        # Create an engine: no controller and no internal dynamics
        engine = jiminy.Engine()
        setup_controller_and_engine(engine, self.robot)

        # Analytical solution
        def sys(t):
            q = 0.0
            v = 0.0
            for i, force in enumerate(F_register):
                if t > force["t"]:
                    pos = self.l * np.array(
                        [-np.cos(q - np.pi / 2), 0.0,
                         np.sin(q - np.pi / 2)])
                    n = pos / np.linalg.norm(pos)
                    d = np.cross(self.axis, n)
                    F_proj = force["F"][:3].T.dot(d)
                    v_delta = ((F_proj + force["F"][4] / self.l) *
                               min(force["dt"], t - force["t"])) / self.m
                    if (i < len(F_register) - 1):
                        q += (v + v_delta) * max(0, min(t,
                            F_register[i + 1]["t"]) - \
                                (force["t"] + force["dt"]))
                    else:
                        q += (v + v_delta) * max(0,
                                                 t - force["t"] + force["dt"])
                    q += (v + v_delta / 2) * min(force["dt"], t - force["t"])
                    v += v_delta
                else:
                    break
            return np.array([q, v])

        # Register a set of impulse forces
        np.random.seed(0)
        F_register = [{
            "t": 0.0,
            "dt": 2.0e-3,
            "F": np.array([1.0e3, 0.0, 0.0, 0.0, 0.0, 0.0])
        }, {
            "t": 0.1,
            "dt": 1.0e-3,
            "F": np.array([0.0, 1.0e3, 0.0, 0.0, 0.0, 0.0])
        }, {
            "t": 0.2,
            "dt": 2.0e-5,
            "F": np.array([-1.0e5, 0.0, 0.0, 0.0, 0.0, 0.0])
        }, {
            "t": 0.2,
            "dt": 2.0e-4,
            "F": np.array([0.0, 0.0, 1.0e4, 0.0, 0.0, 0.0])
        }, {
            "t": 0.4,
            "dt": 1.0e-5,
            "F": np.array([0.0, 0.0, 0.0, 0.0, 2.0e4, 0.0])
        }, {
            "t": 0.4,
            "dt": 1.0e-5,
            "F": np.array([1.0e3, 1.0e4, 3.0e4, 0.0, 0.0, 0.0])
        }, {
            "t": 0.6,
            "dt": 1.0e-6,
            "F": (2.0 * (np.random.rand(6) - 0.5)) * 4.0e6
        }, {
            "t": 0.8,
            "dt": 2.0e-6,
            "F": np.array([0.0, 0.0, 2.0e5, 0.0, 0.0, 0.0])
        }]
        for f in F_register:
            engine.register_force_impulse("PendulumLink", f["t"], f["dt"],
                                          f["F"])

        # Configure the engine: No gravity + Continuous time simulation
        engine_options = engine.get_options()
        engine_options["world"]["gravity"] = np.zeros(6)
        engine_options["stepper"]["sensorsUpdatePeriod"] = 0.0
        engine_options["stepper"]["controllerUpdatePeriod"] = 0.0
        engine_options["stepper"]["logInternalStepperSteps"] = True
        engine.set_options(engine_options)

        # Run simulation and extract some information from log data
        x0 = np.array([0.0, 0.0])
        tf = 1.0
        time, x_jiminy = simulate_and_get_state_evolution(engine,
                                                          tf,
                                                          x0,
                                                          split=False)

        # Compute the associated analytical solution
        x_analytical = np.stack([sys(t) for t in time], axis=0)

        # Check if t = t_start / t_end were breakpoints.
        # Note that the accuracy for the log is 1us.
        t_break_err = np.concatenate([
            np.array(
                [min(abs(f["t"] - time)),
                 min(abs(f["t"] + f["dt"] - time))]) for f in F_register
        ])
        self.assertTrue(np.allclose(t_break_err, 0.0, atol=TOLERANCE))

        # This test has a specific tolerance because the analytical solution is
        # an approximation since in practice, the external force is not
        # constant over its whole application duration but rather depends on
        # the orientation of the pole. For simplicity, the effect of the
        # impulse forces is assumed to be constant. As a result, the tolerance
        # cannot be tighter.
        self.assertTrue(np.allclose(x_jiminy, x_analytical, atol=1e-6))

        # Configure the engine: No gravity + Discrete time simulation
        engine_options = engine.get_options()
        engine_options["world"]["gravity"] = np.zeros(6)
        engine_options["stepper"]["sensorsUpdatePeriod"] = 0.0
        engine_options["stepper"]["controllerUpdatePeriod"] = 0.0
        engine_options["stepper"]["logInternalStepperSteps"] = True
        engine.set_options(engine_options)

        # Configure the engine: Continuous time simulation
        engine_options["stepper"]["sensorsUpdatePeriod"] = 1.0e-3
        engine_options["stepper"]["controllerUpdatePeriod"] = 1.0e-3
        engine.set_options(engine_options)

        # Run simulation
        time, x_jiminy = simulate_and_get_state_evolution(engine,
                                                          tf,
                                                          x0,
                                                          split=False)

        # Compute the associated analytical solution
        x_analytical = np.stack([sys(t) for t in time], axis=0)

        # Check if t = t_start / t_end were breakpoints
        t_break_err = np.concatenate([
            np.array(
                [min(abs(f["t"] - time)),
                 min(abs(f["t"] + f["dt"] - time))]) for f in F_register
        ])
        self.assertTrue(np.allclose(t_break_err, 0.0, atol=TOLERANCE))

        # Compare the numerical and analytical solution
        self.assertTrue(np.allclose(x_jiminy, x_analytical, atol=1e-6))
    def test_constraint_external_force(self):
        """
        @brief    Test support of external force applied with constraints.

        @details  To provide a non-trivial test case with an external force
                  non-colinear to the constraints, simulate two masses
                  oscillating, one along the x axis and the other along the y
                  axis, with a spring between them (in diagonal). The system
                  may be represented as such ((f) indicates fixed bodies)

                  [M_22 (f)]
                      ^
                      ^
                    [M_21]\<>\
                      ^     \<>\
                      ^       \<>\
                      ^         \<>\
                    [O (f)] <><> [M_11] <><> [M_12 (f)]
        """
        # Build two robots with freeflyers, with a freeflyer and a fixed second
        # body constraint.

        # Rebuild the model with a freeflyer
        robots = [jiminy.Robot(), jiminy.Robot()]
        engine = jiminy.EngineMultiRobot()

        # Configure the engine
        engine_options = engine.get_options()
        engine_options["stepper"]["solver"] = "runge_kutta_dopri5"
        engine_options["stepper"]["tolAbs"] = TOLERANCE * 1e-1
        engine_options["stepper"]["tolRel"] = TOLERANCE * 1e-1
        engine.set_options(engine_options)

        # Define some internal parameters
        systems_names = ['FirstSystem', 'SecondSystem']
        k = np.array([[100, 50], [80, 120]])
        nu = np.array([[0.2, 0.01], [0.05, 0.1]])
        k_cross = 100

        # Initialize and configure the engine
        for i in [0, 1]:
            # Load robot
            robots[i] = load_urdf_default(
                self.urdf_name, self.motors_names, has_freeflyer=True)

            # Apply constraints
            freeflyer_constraint = jiminy.FixedFrameConstraint("world")
            robots[i].add_constraint("world", freeflyer_constraint)
            fix_mass_constraint = jiminy.FixedFrameConstraint("SecondMass")
            robots[i].add_constraint("fixMass", fix_mass_constraint)

            # Create controller
            class Controller:
                def __init__(self, k, nu):
                    self.k = k
                    self.nu = nu

                def compute_command(self, t, q, v, sensors_data, command):
                    pass

                def internal_dynamics(self, t, q, v, sensors_data, u_custom):
                    u_custom[6:] = - self.k * q[7:] - self.nu * v[6:]

            controller = Controller(k[i, :], nu[i, :])
            controller = jiminy.BaseControllerFunctor(
                controller.compute_command, controller.internal_dynamics)
            controller.initialize(robots[i])

            # Add system to engine
            engine.add_system(systems_names[i], robots[i], controller)

        # Add coupling force
        def force(t, q1, v1, q2, v2, f):
            # Putting a linear spring between both systems would actually
            # decouple them (the force applied on each system would be a
            # function of this system state only). So we use a nonlinear
            # stiffness, proportional to the square of the distance of both
            # systems to the origin.
            d2 = q1[7] ** 2 + q2[7] ** 2
            f[0] = - k_cross * (1 + d2) * q1[7]
            f[1] = + k_cross * (1 + d2) * q2[7]

        engine.register_force_coupling(
            systems_names[0], systems_names[1], "FirstMass", "FirstMass", force)

        # Initialize the whole system.
        x_init = {}

        ## First system: freeflyer at identity
        x_init['FirstSystem'] = np.zeros(17)
        x_init['FirstSystem'][7:9] = self.x0[:2]
        x_init['FirstSystem'][-2:] = self.x0[2:]
        x_init['FirstSystem'][6] = 1.0

        ## Second system: rotation by pi/2 around Z to bring X axis to Y axis
        x_init['SecondSystem'] = x_init['FirstSystem'].copy()
        x_init['SecondSystem'][5:7] = np.sqrt(2) / 2.0

        # Run simulation and extract some information from log data
        time, x_jiminy = simulate_and_get_state_evolution(
            engine, self.tf, x_init, split=False)

        # Verify that both freeflyers didn't moved
        for x in x_jiminy:
            self.assertTrue(np.allclose(x[:, 9:15], 0.0, atol=TOLERANCE))
            self.assertTrue(np.allclose(x[:, :7], x[0, :7], atol=TOLERANCE))

        # Extract coordinates in a minimum state vector
        x_jiminy_extract = np.hstack([x[:, [7, 8, 15, 16]] for x in x_jiminy])

        # Define dynamics of this system
        def system_dynamics(t, x):
            # Velocity to position
            dx = np.zeros(8)
            dx[:2] = x[2:4]
            dx[4:6] = x[6:8]

            # Compute acceleration linked to the spring
            for i in range(2):
                dx[2 + 4 * i] = (
                    - k[i, 0] * x[4 * i] - nu[i, 0] * x[2 + 4 * i]
                    + k[i, 1] * x[1 + 4 * i] + nu[i, 1] * x[3 + 4 * i])

            # Coupling force between both system
            dsquared = x[0] ** 2 + x[4] ** 2
            dx[2] += - k_cross * (1 + dsquared) * x[0]
            dx[6] += - k_cross * (1 + dsquared) * x[4]

            for i in [0, 1]:
                # Divide forces by mass
                dx[2 + 4 * i] /= self.m[0]

                # Second mass accelration should be opposite of the first
                dx[3 + 4 * i] = -dx[2 + 4 * i]

            return dx

        x0 = np.hstack([x_init[key][[7, 8, 15, 16]] for key in x_init])
        x_python = integrate_dynamics(time, x0, system_dynamics)
        self.assertTrue(np.allclose(
            x_jiminy_extract, x_python, atol=TOLERANCE))