diff --git a/CMakeLists.txt b/CMakeLists.txt
index 12a6ea10e1a72d4710ebc588177703815bc812ae..c7bd1763dfe4608861a977d61c0dfb04a4dc3670 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -17,19 +17,8 @@ set(ARMARX_ENABLE_AUTO_CODE_FORMATTING TRUE)
 armarx_project("RobotAPI")
 depends_on_armarx_package(ArmarXGui)
 
-set(ArmarX_Simox_VERSION 2.3.72)
-option(REQUIRE_SIMOX "If enabled the Simox dependency is a required dependency" TRUE)
-
-if(REQUIRE_SIMOX)
-    find_package(Simox ${ArmarX_Simox_VERSION} REQUIRED)
-else()
-    find_package(Simox ${ArmarX_Simox_VERSION} QUIET)
-endif()
-
 add_subdirectory(source)
 
-list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "simox (= ${ArmarX_Simox_VERSION})")
-
 install_project()
 
 add_subdirectory(scenarios)
diff --git a/etc/cmake/UseRobotAPI.cmake b/etc/cmake/UseRobotAPI.cmake
index de1c501c0780d4b604ccb1d2637d7fa4a222de51..ddbb6ceada4a458087550ecb9ecff5032286c101 100644
--- a/etc/cmake/UseRobotAPI.cmake
+++ b/etc/cmake/UseRobotAPI.cmake
@@ -1,3 +1 @@
 # This file contains macros for projects depending on RobotAPI
-find_package(Simox QUIET)
-find_package(Eigen3 QUIET)
diff --git a/source/RobotAPI/components/DebugDrawer/DebugDrawerHelper.h b/source/RobotAPI/components/DebugDrawer/DebugDrawerHelper.h
index 5656bc799e424199554b7302d21bf98b12e5f82e..b2e14c046bce4e8a9ad572e3875aefe958fa0101 100644
--- a/source/RobotAPI/components/DebugDrawer/DebugDrawerHelper.h
+++ b/source/RobotAPI/components/DebugDrawer/DebugDrawerHelper.h
@@ -24,7 +24,7 @@
 #pragma once
 
 #include <VirtualRobot/Robot.h>
-#include <VirtualRobot/math/OrientedBox.h>
+#include <SimoxUtility/eigen/OrientedBox.h>
 
 #include <RobotAPI/interface/visualization/DebugDrawerInterface.h>
 #include <RobotAPI/libraries/core/Pose.h>
@@ -87,7 +87,7 @@ namespace armarx::detail::DebugDrawerHelper
         void drawBox(const std::string& name, const Eigen::Vector3f& position, float size, const DrawColor& color);
         void drawBox(const std::string& name, const Eigen::Matrix4f& pose, float size, const DrawColor& color);
         template<class FloatT>
-        void drawBox(const std::string& name, const VirtualRobot::OrientedBox<FloatT>& box, const DrawColor& color)
+        void drawBox(const std::string& name, const simox::OrientedBox<FloatT>& box, const DrawColor& color)
         {
             drawBox(name, box.template transformation_centered<float>(), box.template dimensions<float>(), color);
         }
diff --git a/source/RobotAPI/libraries/CMakeLists.txt b/source/RobotAPI/libraries/CMakeLists.txt
index 1c82523cb9ef65c454605423f4fbc44e56c92bf3..4499ff645801a7041b1c280b432dfa6526011d46 100644
--- a/source/RobotAPI/libraries/CMakeLists.txt
+++ b/source/RobotAPI/libraries/CMakeLists.txt
@@ -1,15 +1,15 @@
-add_subdirectory(core)
 add_subdirectory(ArmarXEtherCAT)
-add_subdirectory(KITGripperEtherCAT)
-add_subdirectory(widgets)
-add_subdirectory(SimpleJsonLogger)
-add_subdirectory(RobotAPIVariantWidget)
-add_subdirectory(RobotAPINJointControllers)
-add_subdirectory(DMPController)
+add_subdirectory(ControllerUIUtility)
+add_subdirectory(core)
+add_subdirectory(DebugDrawerConfigWidget)
 # disabled until Mathlib include is fixed...
+add_subdirectory(DMPController)
 add_subdirectory(DSControllers)
+add_subdirectory(KITGripperEtherCAT)
+add_subdirectory(RobotAPIComponentPlugins)
+add_subdirectory(RobotAPINJointControllers)
+add_subdirectory(RobotAPIVariantWidget)
 add_subdirectory(RobotStatechartHelpers)
-add_subdirectory(ControllerUIUtility)
-add_subdirectory(DebugDrawerConfigWidget)
-
-add_subdirectory(RobotAPIComponentPlugins)
\ No newline at end of file
+add_subdirectory(SimpleJsonLogger)
+add_subdirectory(SimpleTrajectory)
+add_subdirectory(widgets)
diff --git a/source/RobotAPI/libraries/SimpleTrajectory/CMakeLists.txt b/source/RobotAPI/libraries/SimpleTrajectory/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5e24563b59a6d4d470916e244a451a6878d07436
--- /dev/null
+++ b/source/RobotAPI/libraries/SimpleTrajectory/CMakeLists.txt
@@ -0,0 +1,31 @@
+set(LIB_NAME       RobotAPISimpleTrajectory)
+
+armarx_component_set_name("${LIB_NAME}")
+armarx_set_target("Library: ${LIB_NAME}")
+
+
+set(LIBS
+    ArmarXCoreInterfaces ArmarXCore
+)
+
+set(LIB_FILES
+    exceptions.cpp
+    Trajectory.cpp
+    Track.cpp
+
+    interpolate/linear.cpp
+)
+set(LIB_HEADERS
+    exceptions.h
+    Track.h
+    Trajectory.h
+    VariantValue.h
+
+    interpolate/linear.h
+)
+
+
+armarx_add_library("${LIB_NAME}" "${LIB_FILES}" "${LIB_HEADERS}" "${LIBS}")
+
+# add unit tests
+add_subdirectory(test)
diff --git a/source/RobotAPI/libraries/SimpleTrajectory/Track.cpp b/source/RobotAPI/libraries/SimpleTrajectory/Track.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6f6bd2970e4f0233510753e1ea6c5354e5fbff0a
--- /dev/null
+++ b/source/RobotAPI/libraries/SimpleTrajectory/Track.cpp
@@ -0,0 +1,16 @@
+#include "Track.h"
+
+
+namespace armarx::trajectory
+{
+
+    template <>
+    void Track<VariantValue>::checkValueType(const VariantValue& value)
+    {
+        if (!empty() && value.type() != keyframes.front().value.type())
+        {
+            throw error::WrongValueTypeInKeyframe(id, value.type(), keyframes.front().value.type());
+        }
+    }
+
+}
diff --git a/source/RobotAPI/libraries/SimpleTrajectory/Track.h b/source/RobotAPI/libraries/SimpleTrajectory/Track.h
new file mode 100644
index 0000000000000000000000000000000000000000..6658b8df8856589c99fbe3cdfe3cfb2bc0f292ce
--- /dev/null
+++ b/source/RobotAPI/libraries/SimpleTrajectory/Track.h
@@ -0,0 +1,284 @@
+#pragma once
+
+#include "VariantValue.h"
+#include "interpolate/linear.h"
+
+
+namespace armarx::trajectory
+{
+
+    /**
+     * @brief A keyframe, representing a value at a given time.
+     */
+    template <typename ValueT>
+    struct Keyframe
+    {
+        /// Constructor.
+        Keyframe(float time, const ValueT& value) : time(time), value(value)
+        {}
+
+        float time;   ///< The time on the timeline.
+        ValueT value; ///< The value.
+    };
+
+    /// A keyframe with of type TValue.
+    using VariantKeyframe = Keyframe<VariantValue>;
+
+
+    /**
+     * @brief A track represents the timeline of a single value, identified by a track ID.
+     * A track is comprised of a sequence of keyframes and used to call a
+     * single update function.
+     */
+    template <typename ValueT>
+    class Track
+    {
+    public:
+        /// The update function type.
+        using UpdateFunc = std::function<void(ValueT)>;
+        // Forward declaration.
+        struct KeyframeProxy;
+
+    public:
+
+        /// Construct a track with given ID (and no update function).
+        Track(const TrackID& id) :
+            id(id)
+        {}
+
+        /// Construct a track with given ID and update function.
+        Track(const TrackID& id, UpdateFunc updateFunc) :
+            id(id), updateFunc(updateFunc)
+        {}
+
+
+        /// Indicate whether this track does not contain any keyframes.
+        bool empty() const;
+        /// Clear the track of all keyframes.
+        void clear();
+
+        /// Add a keyframe to this track.
+        void addKeyframe(const Keyframe<ValueT>& keyframe);
+        /// Add a keyframe to this track.
+        void addKeyframe(float time, const ValueT& value);
+
+        /// Add a keyframe by assignment: `track[time] = value;`
+        KeyframeProxy operator[](float time);
+
+
+        /**
+         * @brief Get the interpolated value at the given time.
+         * @throws `error::EmptyTrack` If the track is empty.
+         */
+        ValueT at(float time) const;
+
+        /**
+         * @brief Call the update function with the interpolated value at the given time.
+         *
+         * @param ignoreIfEmpty If true and the track is empty, the method does nothing.
+         * @throws `error::EmptyTrack` If the track is empty and `ignoreIfEmpty` is false.
+         */
+        void update(float time, bool ignoreIfEmpty = false);
+
+        template <typename V>
+        friend std::ostream& operator<<(std::ostream& os, const Track<V>& track);
+
+
+    private:
+
+        /// Sort the keyframes.
+        void sortKeyframes();
+
+        /**
+         * @throw `error::WrongValueTypeInKeyframe` If the given value's type
+         * conflicts with contained keyframes.
+         */
+        void checkValueType(const ValueT& value);
+
+
+        /// The track ID.
+        TrackID id;
+        /// The update function.
+        UpdateFunc updateFunc;
+        /// The sorted array of keyframes.
+        std::vector<Keyframe<ValueT>> keyframes;
+
+
+    public:
+
+        /// A proxy allowing for adding keyframes by: `track[time] = value;`
+        struct KeyframeProxy
+        {
+        public:
+            /// Get the value at time.
+            operator ValueT() const;
+            /// Add a keyframe on assignment.
+            void operator= (const ValueT& value);
+            /// Add a keyframe on assignment.
+            void operator= (const KeyframeProxy& value);
+
+            /// Get the value at time.
+            ValueT value() const;
+
+        private:
+            friend Track; // Allow TrackBase to use constructor.
+
+            KeyframeProxy(Track& track, float time);
+
+            Track& track;
+            float time;
+        };
+
+    };
+
+    /// A track with value type TValue.
+    using VariantTrack = Track<VariantValue>;
+
+    template <typename V>
+    bool Track<V>::empty() const
+    {
+        return keyframes.empty();
+    }
+
+    template<typename V>
+    void Track<V>::clear()
+    {
+        keyframes.clear();
+    }
+
+    template <typename V>
+    void Track<V>::addKeyframe(const Keyframe<V>& keyframe)
+    {
+        checkValueType(keyframe.value);
+        keyframes.push_back(keyframe);
+        sortKeyframes();
+    }
+
+    template <typename V>
+    void Track<V>::addKeyframe(float time, const V& value)
+    {
+        addKeyframe(Keyframe<V>(time, value));
+    }
+
+    template<typename V>
+    auto Track<V>::operator[](float time) -> KeyframeProxy
+    {
+        return KeyframeProxy{*this, time};
+    }
+
+    template <typename V>
+    void Track<V>::sortKeyframes()
+    {
+        std::sort(keyframes.begin(), keyframes.end(),
+                  [](const Keyframe<V>& lhs, const Keyframe<V>& rhs)
+        {
+            return lhs.time < rhs.time;
+        });
+    }
+
+    /// In general, do nothing.
+    template <typename V>
+    void Track<V>::checkValueType(const V&)
+    {}
+
+    /**
+     * If the track is not empty, check whether the given type of the given
+     * value is the same as the type of the values already in the track.
+     *
+     * @throws `error::WrongValueTypeInKeyframe` if the types do not match.
+     */
+    template <>
+    void Track<VariantValue>::checkValueType(const VariantValue& value);
+
+
+    template <typename V>
+    V Track<V>::at(float time) const
+    {
+        if (empty())
+        {
+            throw error::EmptyTrack(id);
+        }
+
+        if (keyframes.size() == 1)
+        {
+            return keyframes.front().value;
+        }
+
+        if (time <= keyframes.front().time)
+        {
+            return keyframes.front().value;
+        }
+        if (time >= keyframes.back().time)
+        {
+            return keyframes.back().value;
+        }
+
+
+        std::size_t i = 0;
+        while (i + 1 <= keyframes.size() && keyframes[i + 1].time < time)
+        {
+            i++;
+        }
+
+        // interpolate between i and i+1
+        const Keyframe<V>& kf1 = keyframes.at(i);
+        const Keyframe<V>& kf2 = keyframes.at(i + 1);
+
+        float t = (time - kf1.time) / (kf2.time - kf1.time);
+        // t = 0 =>  full kf1, t = 1 => full kf2
+
+        return interpolate::linear<V>(t, kf1.value, kf2.value);
+    }
+
+
+    template <typename V>
+    void Track<V>::update(float time, bool ignoreIfEmpty)
+    {
+        if (updateFunc && !(ignoreIfEmpty && empty()))
+        {
+            updateFunc(at(time));
+        }
+    }
+
+
+    template<typename V>
+    Track<V>::KeyframeProxy::KeyframeProxy(Track& track, float time) :
+        track(track), time(time)
+    {}
+
+    template <typename V>
+    Track<V>::KeyframeProxy::operator V() const
+    {
+        return track.at(time);
+    }
+
+    template <typename V>
+    void Track<V>::KeyframeProxy::operator=(const V& value)
+    {
+        track.addKeyframe(time, value);
+    }
+
+    template <typename V>
+    void Track<V>::KeyframeProxy::operator=(const KeyframeProxy& other)
+    {
+        track.addKeyframe(time, other.value());
+    }
+
+    template <typename V>
+    auto Track<V>::KeyframeProxy::value() const -> V
+    {
+        return track.at(time);
+    }
+
+    template <typename ValueT>
+    std::ostream& operator<<(std::ostream& os, const Track<ValueT>& track)
+    {
+        os << "Track '" << track.id << "' with " << track.keyframes.size() << " keyframes: [";
+        for (const Keyframe<ValueT>& kf : track.keyframes)
+        {
+            os << kf.time << ", ";
+        }
+        return os << "]";
+    }
+
+}
diff --git a/source/RobotAPI/libraries/SimpleTrajectory/Trajectory.cpp b/source/RobotAPI/libraries/SimpleTrajectory/Trajectory.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5211a504403ce4dff2feca568998ac4cd88ca555
--- /dev/null
+++ b/source/RobotAPI/libraries/SimpleTrajectory/Trajectory.cpp
@@ -0,0 +1,105 @@
+#include "Trajectory.h"
+
+
+using namespace armarx::trajectory;
+
+
+Trajectory::Trajectory()
+{}
+
+void Trajectory::clear()
+{
+    tracks.clear();
+}
+
+VariantTrack& Trajectory::addTrack(const TrackID& id)
+{
+    return tracks.emplace(id, VariantTrack {id}).first->second;
+}
+
+VariantTrack& Trajectory::addTrack(const TrackID& id, const VariantTrack::UpdateFunc& updateFunc)
+{
+    return tracks.emplace(id, VariantTrack {id, updateFunc}).first->second;
+}
+
+void Trajectory::addKeyframe(const TrackID& id, const VariantKeyframe& keyframe)
+{
+    (*this)[id].addKeyframe(keyframe);
+}
+
+void Trajectory::addKeyframe(const TrackID& id, float time, const VariantValue& value)
+{
+    addKeyframe(id, VariantKeyframe {time, value});
+}
+
+void Trajectory::update(float time, bool ignoreEmptyTracks)
+{
+    for (auto& it : tracks)
+    {
+        it.second.update(time, ignoreEmptyTracks);
+    }
+}
+
+VariantTrack& Trajectory::operator[](const TrackID& id)
+{
+    try
+    {
+        return tracks.at(id);
+    }
+    catch (const std::out_of_range&)
+    {
+        throw error::NoTrackWithID(id);
+    }
+}
+
+const VariantTrack& Trajectory::operator[](const TrackID& id) const
+{
+    try
+    {
+        return tracks.at(id);
+    }
+    catch (const std::out_of_range&)
+    {
+        throw error::NoTrackWithID(id);
+    }
+}
+
+std::ostream& armarx::trajectory::operator<<(std::ostream& os, const Trajectory& trajectory)
+{
+    os << "Trajectory with " << trajectory.tracks.size() << " tracks: ";
+    for (const auto& [name, track] : trajectory.tracks)
+    {
+        os << "\n  - " << track;
+    }
+    return os;
+}
+
+
+namespace armarx
+{
+
+    auto trajectory::toUpdateFunc(std::function<void (float)> func) -> VariantTrack::UpdateFunc
+    {
+        return [func](VariantValue value)
+        {
+            func(boost::get<float>(value));
+        };
+    }
+
+    auto trajectory::toUpdateFunc(std::function<void (const Eigen::MatrixXf&)> func) -> VariantTrack::UpdateFunc
+    {
+        return [func](VariantValue value)
+        {
+            func(boost::get<Eigen::MatrixXf>(value));
+        };
+    }
+
+    auto trajectory::toUpdateFunc(std::function<void (const Eigen::Quaternionf&)> func) -> VariantTrack::UpdateFunc
+    {
+        return [func](VariantValue value)
+        {
+            func(boost::get<Eigen::Quaternionf>(value));
+        };
+    }
+
+}
diff --git a/source/RobotAPI/libraries/SimpleTrajectory/Trajectory.h b/source/RobotAPI/libraries/SimpleTrajectory/Trajectory.h
new file mode 100644
index 0000000000000000000000000000000000000000..5600a669fa6bf6afce56ff08d3f2a947912fd139
--- /dev/null
+++ b/source/RobotAPI/libraries/SimpleTrajectory/Trajectory.h
@@ -0,0 +1,105 @@
+#pragma once
+
+#include <map>
+
+#include "Track.h"
+
+
+namespace armarx::trajectory
+{
+
+    /**
+     * @brief This class is used to update entities based on trajectory
+     * defined by keyframes.
+     *
+     * A Trajectory is a collection of tracks, where each track represents a
+     * timeline of a single value. A track is composed of keyframes, where
+     * each keyframe represents a value at a specific time on the timeline.
+     *
+     * @see Track
+     */
+    class Trajectory
+    {
+    public:
+
+        /// Constructor.
+        Trajectory();
+
+
+        /// Clear the trajectory of all tracks.
+        void clear();
+
+
+        /**
+         * @brief Add track with the given ID (and no update function).
+         * If there is already a track with the given ID, it is overwritten.
+         */
+        VariantTrack& addTrack(const TrackID& id);
+
+        /**
+         * @brief Add track with the given ID and update function.
+         * If there is already a track with the given ID, it is overwritten.
+         */
+        VariantTrack& addTrack(const TrackID& id, const VariantTrack::UpdateFunc& updateFunc);
+
+
+        /// Add a keyframe to the specified track.
+        /// @throw `error::NoTrackWithID` if there is not track with the given ID
+        void addKeyframe(const TrackID& id, const VariantKeyframe& keyframe);
+
+        /// Add a keyframe to the specified track.
+        /// @throw `error::NoTrackWithID` if there is not track with the given ID
+        void addKeyframe(const TrackID& id, float time, const VariantValue& value);
+
+
+        /**
+         * @brief Update all tracks for the given time.
+         * @param ignoreEmptyTracks If true, empty tracks are ignored.
+         * @throws `error::EmptyTrack` If `ignoreEmptyTracks` is false and a track is empty.
+         */
+        void update(float time, bool ignoreEmptyTracks = false);
+
+
+        /// Get the track with the given ID.
+        /// @throw `error::NoTrackWithID` if there is not track with the given ID
+        VariantTrack& operator[](const TrackID& id);
+
+        /// Get the track with the given ID.
+        /// @throw `error::NoTrackWithID` if there is not track with the given ID
+        const VariantTrack& operator[](const TrackID& id) const;
+
+        friend std::ostream& operator<<(std::ostream& os, const Trajectory& trajectory);
+
+
+    private:
+
+        /// The tracks.
+        std::map<TrackID, VariantTrack> tracks;
+
+    };
+
+
+    std::ostream& operator<<(std::ostream& os, const Trajectory& trajectory);
+
+
+    // UPDATE FUNCTION HELPERS
+
+    /// Get an update function for value assignments.
+    template <typename ValueT>
+    VariantTrack::UpdateFunc updateValue(ValueT& v)
+    {
+        return [&v](VariantValue value)
+        {
+            v = boost::get<ValueT>(value);
+        };
+    }
+
+    /// Wrap the function in a Track::UpdateFunc.
+    VariantTrack::UpdateFunc toUpdateFunc(std::function<void(float)> func);
+    /// Wrap the function in a Track::UpdateFunc.
+    VariantTrack::UpdateFunc toUpdateFunc(std::function<void(const Eigen::MatrixXf&)> func);
+    /// Wrap the function in a Track::UpdateFunc.
+    VariantTrack::UpdateFunc toUpdateFunc(std::function<void(const Eigen::Quaternionf&)> func);
+
+}
+
diff --git a/source/RobotAPI/libraries/SimpleTrajectory/VariantValue.h b/source/RobotAPI/libraries/SimpleTrajectory/VariantValue.h
new file mode 100644
index 0000000000000000000000000000000000000000..c7e6db669445a95033f304da8d4eb0da0fb9890b
--- /dev/null
+++ b/source/RobotAPI/libraries/SimpleTrajectory/VariantValue.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <boost/variant.hpp>
+
+#include <Eigen/Eigen>
+
+
+namespace armarx::trajectory
+{
+
+    /// Variant for trajectory values.
+    using VariantValue = boost::variant<float, Eigen::MatrixXf, Eigen::Quaternionf>;
+
+    /// ID of tracks.
+    using TrackID = std::string;
+
+}
diff --git a/source/RobotAPI/libraries/SimpleTrajectory/exceptions.cpp b/source/RobotAPI/libraries/SimpleTrajectory/exceptions.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5e3db572f972e0b55bfa399b774d328c04603473
--- /dev/null
+++ b/source/RobotAPI/libraries/SimpleTrajectory/exceptions.cpp
@@ -0,0 +1,51 @@
+#include "exceptions.h"
+
+using namespace armarx::trajectory::error;
+
+
+TrajectoryException::TrajectoryException(const std::string& msg) : std::logic_error(msg)
+{}
+
+
+InterpolateDifferentTypesError::InterpolateDifferentTypesError() :
+    TrajectoryException("Interpolating between two different types.")
+{}
+
+
+NoTrackWithID::NoTrackWithID(const TrackID& id) : TrajectoryException(makeMsg(id))
+{}
+
+std::string NoTrackWithID::makeMsg(const TrackID& id)
+{
+    std::stringstream ss;
+    ss << "No track with ID '" << id << "'. \n"
+       << "Add a track with ID '" << id << "' before before adding keyframes.";
+    return ss.str();
+}
+
+
+EmptyTrack::EmptyTrack(const TrackID& id) : TrajectoryException(makeMsg(id))
+{}
+
+std::string EmptyTrack::makeMsg(const TrackID& id)
+{
+    std::stringstream ss;
+    ss << "Track with ID '" << id << "' is empty. \n"
+       "Add a keyframe to track '" << id << "' before updating.";
+    return ss.str();
+}
+
+WrongValueTypeInKeyframe::WrongValueTypeInKeyframe(
+    const TrackID& trackID, const std::type_info& type, const std::type_info& expected) :
+    TrajectoryException(makeMsg(trackID, type, expected))
+{}
+
+std::string WrongValueTypeInKeyframe::makeMsg(
+    const TrackID& id, const std::type_info& type, const std::type_info& expected)
+{
+    std::stringstream ss;
+    ss << "Tried to add keyframe with value type '" << type.name() << "' to non-empty track '"
+       << id << "' containing values of type '" << expected.name() << "'. \n"
+       << "Only one value type per track is allowed.";
+    return ss.str();
+}
diff --git a/source/RobotAPI/libraries/SimpleTrajectory/exceptions.h b/source/RobotAPI/libraries/SimpleTrajectory/exceptions.h
new file mode 100644
index 0000000000000000000000000000000000000000..6235ef3f737fe97359f4da72c25ec6c870a49da2
--- /dev/null
+++ b/source/RobotAPI/libraries/SimpleTrajectory/exceptions.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include <sstream>
+#include <stdexcept>
+
+#include "VariantValue.h" // for TrackID
+
+
+namespace armarx::trajectory::error
+{
+
+
+    struct TrajectoryException : public std::logic_error
+    {
+        TrajectoryException(const std::string& msg);
+    };
+
+
+    struct InterpolateDifferentTypesError : public TrajectoryException
+    {
+        InterpolateDifferentTypesError();
+    };
+
+
+    struct NoTrackWithID : public TrajectoryException
+    {
+        NoTrackWithID(const TrackID& id);
+        static std::string makeMsg(const TrackID& id);
+    };
+
+
+    struct EmptyTrack : public TrajectoryException
+    {
+        EmptyTrack(const TrackID& id);
+        static std::string makeMsg(const TrackID& id);
+    };
+
+
+    struct WrongValueTypeInKeyframe : public TrajectoryException
+    {
+        WrongValueTypeInKeyframe(const TrackID& trackID, const std::type_info& type,
+                                 const std::type_info& expected);
+        static std::string makeMsg(const TrackID& trackID, const std::type_info& type,
+                                   const std::type_info& expected);
+    };
+
+
+
+}
diff --git a/source/RobotAPI/libraries/SimpleTrajectory/interpolate/linear.cpp b/source/RobotAPI/libraries/SimpleTrajectory/interpolate/linear.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c4b41c638d9ae8f3c5100d9f60f1d47743bcb99d
--- /dev/null
+++ b/source/RobotAPI/libraries/SimpleTrajectory/interpolate/linear.cpp
@@ -0,0 +1,14 @@
+#include "linear.h"
+
+
+namespace armarx::trajectory
+{
+
+    template <>
+    VariantValue interpolate::linear<VariantValue>(float t, const VariantValue& lhs, const VariantValue& rhs)
+    {
+        // dont use boost::get
+        return boost::apply_visitor(Linear {t}, lhs, rhs);
+    }
+
+}
diff --git a/source/RobotAPI/libraries/SimpleTrajectory/interpolate/linear.h b/source/RobotAPI/libraries/SimpleTrajectory/interpolate/linear.h
new file mode 100644
index 0000000000000000000000000000000000000000..4cf381844ec819550b1a3bc857b28ee713705a5c
--- /dev/null
+++ b/source/RobotAPI/libraries/SimpleTrajectory/interpolate/linear.h
@@ -0,0 +1,64 @@
+#pragma once
+
+#include "../exceptions.h"
+#include "../VariantValue.h"
+
+
+namespace armarx::trajectory::interpolate
+{
+
+    /**
+     * @brief Linear interpolation visitor: Interpolates between the given values linearly.
+     */
+    class Linear : public boost::static_visitor<>
+    {
+    public:
+
+        using result_type = VariantValue; ///< Exposed result type.
+
+    public:
+
+        /**
+         * @brief Interpolator
+         * @param t in [0, 1], where `t = 0` for `lhs` and `t = 1` for `rhs`.
+         */
+        Linear(float t) : t(t) {}
+
+        template <typename U, typename V>
+        float operator()(const U&, const V&) const
+        {
+            throw error::InterpolateDifferentTypesError();
+        }
+
+        float operator()(const float& lhs, const float& rhs) const
+        {
+            return (1 - t) * lhs + t * rhs;
+        }
+
+        Eigen::MatrixXf operator()(const Eigen::MatrixXf& lhs, const Eigen::MatrixXf& rhs) const
+        {
+            return (1 - t) * lhs + t * rhs;
+        }
+
+        Eigen::Quaternionf operator()(const Eigen::Quaternionf& lhs, const Eigen::Quaternionf& rhs) const
+        {
+            return lhs.slerp(t, rhs);
+        }
+
+
+    private:
+
+        float t;
+
+    };
+
+    template <typename ReturnT>
+    ReturnT linear(float t, const VariantValue& lhs, const VariantValue& rhs)
+    {
+        return boost::get<ReturnT>(boost::apply_visitor(Linear {t}, lhs, rhs));
+    }
+
+    template <>
+    VariantValue linear<VariantValue>(float t, const VariantValue& lhs, const VariantValue& rhs);
+
+}
diff --git a/source/RobotAPI/libraries/SimpleTrajectory/test/CMakeLists.txt b/source/RobotAPI/libraries/SimpleTrajectory/test/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..6d0e31ccf0b662d39fde412ddcd4f070302e7813
--- /dev/null
+++ b/source/RobotAPI/libraries/SimpleTrajectory/test/CMakeLists.txt
@@ -0,0 +1,5 @@
+
+# Libs required for the tests
+SET(LIBS ${LIBS} ArmarXCore ${LIB_NAME})
+
+armarx_add_test(${LIB_NAME}TrajectoryTest TrajectoryTest.cpp "${LIBS}")
diff --git a/source/RobotAPI/libraries/SimpleTrajectory/test/TrajectoryTest.cpp b/source/RobotAPI/libraries/SimpleTrajectory/test/TrajectoryTest.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6f0553c521f68f8a583736d7b12ac59632cffb46
--- /dev/null
+++ b/source/RobotAPI/libraries/SimpleTrajectory/test/TrajectoryTest.cpp
@@ -0,0 +1,266 @@
+
+#define BOOST_TEST_MODULE RobotAPI::ArmarXLibraries::RobotAPISimpleTrajectory
+
+#define ARMARX_BOOST_TEST
+
+#include <iostream>
+
+#include <RobotAPI/Test.h>
+#include "../Trajectory.h"
+
+
+using namespace armarx::trajectory;
+
+namespace std
+{
+    std::ostream& operator<<(std::ostream& os, const std::type_info& rhs)
+    {
+        os << "TypeInfo (" << rhs.name() << ", " << rhs.hash_code() << ")";
+        return os;
+    }
+}
+
+
+struct Fixture
+{
+    Trajectory trajectory;
+
+    float float_;
+    Eigen::Vector3f vec;
+    Eigen::MatrixXf mat;
+    Eigen::Quaternionf quat;
+
+    float ftol = 1e-9f;
+};
+
+
+BOOST_FIXTURE_TEST_SUITE(TrajectoryTest, Fixture)
+
+
+BOOST_AUTO_TEST_CASE(testNoTrackWithID)
+{
+    for (auto* id :
+         { "id1", "id2"
+         })
+    {
+        BOOST_CHECK_THROW(trajectory.addKeyframe(id, 0, 0.f), error::NoTrackWithID);
+        BOOST_CHECK_THROW(trajectory[id].addKeyframe(0, 0.f), error::NoTrackWithID);
+        BOOST_CHECK_THROW(trajectory[id][0] = 0.f, error::NoTrackWithID);
+    }
+
+    trajectory.addTrack("id1", updateValue(float_));
+
+    BOOST_CHECK_NO_THROW(trajectory.addKeyframe("id1", 0, 0.f));
+    BOOST_CHECK_NO_THROW(trajectory["id1"].addKeyframe(1, 0.f));
+    BOOST_CHECK_NO_THROW(trajectory["id1"][2] = 0.f);
+
+    BOOST_CHECK_THROW(trajectory.addKeyframe("id2", 0, 0.f), error::NoTrackWithID);
+    BOOST_CHECK_THROW(trajectory["id2"].addKeyframe(0, 0.f), error::NoTrackWithID);
+    BOOST_CHECK_THROW(trajectory["id2"][0] = 0.f, error::NoTrackWithID);
+}
+
+
+BOOST_AUTO_TEST_CASE(testEmptyTrack)
+{
+    trajectory.addTrack("float", updateValue(float_));
+
+    BOOST_CHECK(trajectory["float"].empty());
+
+    BOOST_CHECK_THROW(trajectory["float"].update(0), error::EmptyTrack);
+    BOOST_CHECK_THROW(trajectory.update(0), error::EmptyTrack);
+
+    trajectory.addKeyframe("float", VariantKeyframe {0.f, 1.f});
+
+    BOOST_CHECK_NO_THROW(trajectory["float"].update(0));
+    BOOST_CHECK_NO_THROW(trajectory.update(0));
+    BOOST_CHECK_NO_THROW(trajectory.update(-1));
+    BOOST_CHECK_NO_THROW(trajectory.update(1));
+}
+
+template <typename Derived>
+std::ostream& operator<<(std::ostream& os, const Eigen::MatrixBase<Derived>& rhs)
+{
+    static const Eigen::IOFormat iof(5, 0, " ", "\n", "| ", " |", "");
+    os << rhs.format(iof);
+    return os;
+}
+
+template <typename Derived>
+std::ostream& operator<<(std::ostream& os, const Eigen::QuaternionBase<Derived>& rhs)
+{
+    static const Eigen::IOFormat iof(5, 0, " ", "\n", "| ", " |", "");
+    os << rhs.format(iof);
+    return os;
+}
+
+
+
+
+BOOST_AUTO_TEST_CASE(testDifferentTypes)
+{
+    trajectory.addTrack("float", updateValue(float_));
+
+    std::vector<VariantValue> values
+    {
+        1.f, Eigen::MatrixXf::Zero(3, 3), Eigen::Quaternionf::Identity()
+    };
+
+    for (std::size_t i = 1; i < values.size(); ++i)
+    {
+        for (std::size_t j = 0; j < i; ++j)
+        {
+            BOOST_CHECK_NE(values[i].type(), values[j].type());
+        }
+    }
+
+    BOOST_CHECK_NO_THROW(trajectory.addKeyframe("float", 0, 0.f));
+    BOOST_CHECK_THROW(trajectory.addKeyframe("float", 1, Eigen::MatrixXf::Zero(3, 3)),
+                      error::WrongValueTypeInKeyframe);
+    BOOST_CHECK_THROW(trajectory.addKeyframe("float", 1, Eigen::Quaternionf::Identity()),
+                      error::WrongValueTypeInKeyframe);
+}
+
+BOOST_AUTO_TEST_CASE(testInterpolateFloat)
+{
+    float f1 = -2;
+    float f2 =  5;
+
+    VariantValue v1(f1);
+    VariantValue v2(f2);
+
+    BOOST_CHECK_EQUAL(interpolate::linear<float>(0, v1, v2), f1);
+    BOOST_CHECK_EQUAL(interpolate::linear<float>(.5, v1, v2), .5f * (f1 + f2));
+    BOOST_CHECK_EQUAL(interpolate::linear<float>(1, v1, v2), f2);
+}
+
+BOOST_AUTO_TEST_CASE(testInterpolateMatrix)
+{
+    Eigen::MatrixXf m1 = Eigen::MatrixXf::Zero(3, 3);
+    Eigen::MatrixXf m2 = Eigen::MatrixXf::Identity(3, 3);
+
+    VariantValue v1(m1);
+    VariantValue v2(m2);
+
+    Eigen::MatrixXf mi;
+
+    mi = interpolate::linear<Eigen::MatrixXf>(0, v1, v2);
+    BOOST_CHECK(mi.isApprox(m1, ftol));
+
+    mi = interpolate::linear<Eigen::MatrixXf>(0.5, v1, v2);
+    BOOST_CHECK(mi.isApprox(0.5f * (m1 + m2), ftol));
+
+    mi = interpolate::linear<Eigen::MatrixXf>(1, v1, v2);
+    BOOST_CHECK(mi.isApprox(m2, ftol));
+}
+
+BOOST_AUTO_TEST_CASE(testInterpolateQuaternion)
+{
+    Eigen::Quaternionf q1(Eigen::AngleAxisf(0.f, Eigen::Vector3f(1, 0, 0)));
+    Eigen::Quaternionf q2(Eigen::AngleAxisf(2.f, Eigen::Vector3f(0, 1, 0)));;
+
+    VariantValue v1(q1);
+    VariantValue v2(q2);
+
+    Eigen::Quaternionf mi;
+
+    mi = interpolate::linear<Eigen::Quaternionf>(0, v1, v2);
+    BOOST_CHECK(mi.isApprox(q1, ftol));
+
+    mi = interpolate::linear<Eigen::Quaternionf>(0.5, v1, v2);
+    BOOST_CHECK(mi.isApprox(q1.slerp(.5, q2), ftol));
+
+    mi = interpolate::linear<Eigen::Quaternionf>(1, v1, v2);
+    BOOST_CHECK(mi.isApprox(q2, ftol));
+}
+
+BOOST_AUTO_TEST_CASE(testInterpolateVector3)
+{
+    using ValueT = Eigen::Vector3f;
+
+    ValueT vec1 = Eigen::Vector3f::Zero();
+    ValueT vec2 = Eigen::Vector3f::Ones();
+
+    VariantValue v1(vec1);
+    VariantValue v2(vec2);
+
+    ValueT mi;
+
+    mi = interpolate::linear<Eigen::MatrixXf>(0, v1, v2);
+    BOOST_CHECK(mi.isApprox(vec1, ftol));
+
+    mi = interpolate::linear<Eigen::MatrixXf>(0.5, v1, v2);
+    BOOST_CHECK(mi.isApprox(0.5 * (vec1 + vec2), ftol));
+
+    mi = interpolate::linear<Eigen::MatrixXf>(1, v1, v2);
+    BOOST_CHECK(mi.isApprox(vec2, ftol));
+}
+
+
+
+BOOST_AUTO_TEST_CASE(testTrajectoryFloat)
+{
+    trajectory.addTrack("float", updateValue(float_));
+
+    trajectory.addKeyframe("float", 0.f, 0.f);
+    trajectory.addKeyframe("float", VariantKeyframe {1.f, 1.f});
+
+    int num = 10;
+
+    for (float i = 0; i <= num; ++i)
+    {
+        float time = -i / num;
+        trajectory.update(time);
+        BOOST_CHECK_CLOSE(float_, 0, ftol);
+    }
+
+    for (float i = 0; i <= num; ++i)
+    {
+        float time = i / num;
+        trajectory.update(time);
+        BOOST_CHECK_CLOSE(float_, time, ftol);
+    }
+
+    for (float i = 0; i <= num; ++i)
+    {
+        float time = 1 + i / num;
+        trajectory.update(time);
+        BOOST_CHECK_CLOSE(float_, 1, ftol);
+    }
+}
+
+BOOST_AUTO_TEST_CASE(testTrajectoryMatrix)
+{
+    trajectory.addTrack("matrix", updateValue(mat));
+
+    trajectory.addKeyframe("matrix", 0.f, Eigen::MatrixXf::Zero(3, 3));
+    trajectory.addKeyframe("matrix", 1.f, Eigen::MatrixXf::Identity(3, 3));
+
+    int num = 10;
+
+    for (float i = 0; i <= num; ++i)
+    {
+        float time = -i / num;
+        trajectory.update(time);
+        BOOST_CHECK_CLOSE(mat(0, 0), 0, ftol);
+        BOOST_CHECK_CLOSE(mat(0, 1), 0, ftol);
+    }
+
+    for (float i = 0; i <= num; ++i)
+    {
+        float time = i / num;
+        trajectory.update(time);
+        BOOST_CHECK_CLOSE(mat(0, 0), time, ftol);
+        BOOST_CHECK_CLOSE(mat(0, 1), 0, ftol);
+    }
+
+    for (float i = 0; i <= num; ++i)
+    {
+        float time = 1 + i / num;
+        trajectory.update(time);
+        BOOST_CHECK_CLOSE(mat(0, 0), 1, ftol);
+        BOOST_CHECK_CLOSE(mat(0, 1), 0, ftol);
+    }
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+