diff --git a/examples/cart_pulling/control_sub_problems/with_planner/base_and_dual_arm_ee_path_following_from_planner.py b/examples/cart_pulling/control_sub_problems/dual_arm_cart_pulling.py
similarity index 100%
rename from examples/cart_pulling/control_sub_problems/with_planner/base_and_dual_arm_ee_path_following_from_planner.py
rename to examples/cart_pulling/control_sub_problems/dual_arm_cart_pulling.py
diff --git a/examples/cart_pulling/control_sub_problems/fixed_paths/base_and_ee_path_following.py b/examples/cart_pulling/control_sub_problems/fixed_paths/base_and_ee_path_following.py
deleted file mode 100644
index f7c1fd338071b2cb6a7e836b4fcc7576cca945f0..0000000000000000000000000000000000000000
--- a/examples/cart_pulling/control_sub_problems/fixed_paths/base_and_ee_path_following.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from smc import getRobotFromArgs
-from smc import getMinimalArgParser
-from smc.control.optimal_control.util import get_OCP_args
-from smc.control.cartesian_space import getClikArgs
-from smc.control.optimal_control.croco_mpc_path_following import (
-    BaseAndEEPathFollowingMPC,
-)
-import numpy as np
-
-
-def path(T_w_e, i):
-    # 2) do T_mobile_base_ee_pos to get
-    # end-effector reference frame(s)
-
-    # generate bullshit just to see it works
-    path_ee = []
-    path_base = []
-    t = i * robot.dt
-    for i in range(args.n_knots):
-        t += 0.01
-        new = T_w_e.copy()
-        translation = 2 * np.array([np.cos(t / 20), np.sin(t / 20), 0.3])
-        new.translation = translation
-        path_ee.append(new)
-        translation[2] = 0.0
-        path_base.append(translation)
-    return path_base, path_ee
-
-
-def get_args():
-    parser = getMinimalArgParser()
-    parser = get_OCP_args(parser)
-    parser = getClikArgs(parser)  # literally just for goal error
-    args = parser.parse_args()
-    return args
-
-
-if __name__ == "__main__":
-    args = get_args()
-    robot = getRobotFromArgs(args)
-    x0 = np.concatenate([robot.q, robot.v])
-    robot._step()
-
-    BaseAndEEPathFollowingMPC(args, robot, path)
-
-    print("final position:", robot.T_w_e)
-
-    if args.real:
-        robot.stopRobot()
-
-    if args.save_log:
-        robot._log_manager.saveLog()
-        robot._log_manager.plotAllControlLoops()
-
-    if args.visualizer:
-        robot.killManipulatorVisualizer()
diff --git a/examples/cart_pulling/control_sub_problems/fixed_paths/dual_arm_path_following.py b/examples/cart_pulling/control_sub_problems/fixed_paths/dual_arm_path_following.py
deleted file mode 100644
index 40021e9420f356e4e10b5b85c5da0ebce7bb9361..0000000000000000000000000000000000000000
--- a/examples/cart_pulling/control_sub_problems/fixed_paths/dual_arm_path_following.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from smc import getRobotFromArgs
-from smc import getMinimalArgParser
-from smc.control.optimal_control.util import get_OCP_args
-from smc.control.cartesian_space import getClikArgs
-from smc.control.optimal_control.croco_mpc_path_following import (
-    CrocoEEPathFollowingMPC,
-)
-import numpy as np
-
-
-def path(T_w_e, i):
-    # 2) do T_mobile_base_ee_pos to get
-    # end-effector reference frame(s)
-
-    # generate bullshit just to see it works
-    path = []
-    t = i * robot.dt
-    for i in range(args.n_knots):
-        t += 0.01
-        new = T_w_e.copy()
-        translation = 2 * np.array([np.cos(t / 20), np.sin(t / 20), 0.3])
-        new.translation = translation
-        path.append(new)
-    return path
-
-
-def get_args():
-    parser = getMinimalArgParser()
-    parser = get_OCP_args(parser)
-    parser = getClikArgs(parser)  # literally just for goal error
-    args = parser.parse_args()
-    return args
-
-
-if __name__ == "__main__":
-    args = get_args()
-    robot = getRobotFromArgs(args)
-    x0 = np.concatenate([robot.q, robot.v])
-    robot._step()
-
-    CrocoDualEEPathFollowingMPC(args, robot, x0, path)
-
-    print("final position:", robot.T_w_e)
-
-    if args.real:
-        robot.stopRobot()
-
-    if args.save_log:
-        robot._log_manager.saveLog()
-        robot._log_manager.plotAllControlLoops()
-
-    if args.visualizer:
-        robot.killManipulatorVisualizer()
diff --git a/examples/cart_pulling/control_sub_problems/fixed_paths/ee_only_path_following.py b/examples/cart_pulling/control_sub_problems/fixed_paths/ee_only_path_following.py
deleted file mode 100644
index 2e11a54104211e46764924edde04603c624414d5..0000000000000000000000000000000000000000
--- a/examples/cart_pulling/control_sub_problems/fixed_paths/ee_only_path_following.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from smc import getRobotFromArgs
-from smc import getMinimalArgParser
-from smc.control.optimal_control.util import get_OCP_args
-from smc.control.cartesian_space import getClikArgs
-from smc.control.optimal_control.croco_mpc_path_following import (
-    CrocoEEPathFollowingMPC,
-)
-import numpy as np
-
-
-def path(T_w_e, i):
-    # 2) do T_mobile_base_ee_pos to get
-    # end-effector reference frame(s)
-
-    # generate bullshit just to see it works
-    path = []
-    t = i * robot.dt
-    for i in range(args.n_knots):
-        t += 0.01
-        new = T_w_e.copy()
-        translation = 2 * np.array([np.cos(t / 20), np.sin(t / 20), 0.3])
-        new.translation = translation
-        path.append(new)
-    return path
-
-
-def get_args():
-    parser = getMinimalArgParser()
-    parser = get_OCP_args(parser)
-    parser = getClikArgs(parser)  # literally just for goal error
-    args = parser.parse_args()
-    return args
-
-
-if __name__ == "__main__":
-    args = get_args()
-    robot = getRobotFromArgs(args)
-    x0 = np.concatenate([robot.q, robot.v])
-    robot._step()
-
-    CrocoEEPathFollowingMPC(args, robot, x0, path)
-
-    print("final position:", robot.T_w_e)
-
-    if args.real:
-        robot.stopRobot()
-
-    if args.save_log:
-        robot._log_manager.saveLog()
-        robot._log_manager.plotAllControlLoops()
-
-    if args.visualizer:
-        robot.killManipulatorVisualizer()
diff --git a/examples/cart_pulling/control_sub_problems/with_planner/base_and_ee_path_following_from_planner.py b/examples/cart_pulling/control_sub_problems/single_arm_cart_pulling.py
similarity index 89%
rename from examples/cart_pulling/control_sub_problems/with_planner/base_and_ee_path_following_from_planner.py
rename to examples/cart_pulling/control_sub_problems/single_arm_cart_pulling.py
index 2d34a8528513c6f8fafbda71ef093e0b260ef59a..993171a1fae2a5a455f527f347f2d85dee603afc 100644
--- a/examples/cart_pulling/control_sub_problems/with_planner/base_and_ee_path_following_from_planner.py
+++ b/examples/cart_pulling/control_sub_problems/single_arm_cart_pulling.py
@@ -6,15 +6,15 @@ from smc.control.optimal_control.util import get_OCP_args
 from smc.control.cartesian_space import getClikArgs
 from smc.path_generation.planner import starPlanner, getPlanningArgs
 from smc.control.optimal_control.croco_mpc_point_to_point import CrocoEEAndBaseP2PMPC
-from smc.control.optimal_control.croco_mpc_path_following import (
-    BaseAndEEPathFollowingMPC,
-)
+from single_arm_cart_pulling_control_loop import SingleArmCartPullingMPC
 from smc.multiprocessing import ProcessManager
 
 import time
 import numpy as np
 from functools import partial
 import pinocchio as pin
+from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface
+from smc.robots.interfaces.single_arm_interface import SingleArmInterface
 
 
 def get_args():
@@ -43,6 +43,8 @@ if __name__ == "__main__":
     robot = getRobotFromArgs(args)
     # TODO: HOW IS IT POSSIBLE THAT T_W_E IS WRONG WITHOUT STEP CALLED HERE?????????????????
     robot._step()
+    assert issubclass(robot.__class__, SingleArmInterface)
+    assert issubclass(robot.__class__, MobileBaseInterface)
     T_w_e = robot.T_w_e
     robot._q[0] = 9.0
     robot._q[1] = 4.0
@@ -88,9 +90,7 @@ if __name__ == "__main__":
         # TODO: UNCOMMENT
         CrocoEEAndBaseP2PMPC(args, robot, pathSE3[0], p_base)
     print("initialized!")
-    BaseAndEEPathFollowingMPC(args, robot, path_planner)
-
-    print("final position:", robot.T_w_e)
+    SingleArmCartPullingMPC(args, robot, path_planner)
 
     if args.real:
         robot.stopRobot()
diff --git a/examples/cart_pulling/control_sub_problems/single_arm_cart_pulling_control_loop.py b/examples/cart_pulling/control_sub_problems/single_arm_cart_pulling_control_loop.py
new file mode 100644
index 0000000000000000000000000000000000000000..8a944a57f2fd5f4daba2d7b1f7c5d4c271d90f55
--- /dev/null
+++ b/examples/cart_pulling/control_sub_problems/single_arm_cart_pulling_control_loop.py
@@ -0,0 +1,135 @@
+from smc.robots.interfaces.whole_body_interface import (
+    SingleArmWholeBodyInterface,
+)
+from smc.control.control_loop_manager import ControlLoopManager
+from smc.multiprocessing.process_manager import ProcessManager
+from smc.control.optimal_control.abstract_croco_ocp import CrocoOCP
+from smc.control.optimal_control.path_following_croco_ocp import (
+    BaseAndEEPathFollowingOCP,
+)
+from smc.path_generation.path_math.cart_pulling_path_math import (
+    construct_EE_path,
+)
+from smc.path_generation.path_math.path_to_trajectory import (
+    path2D_to_trajectory2D,
+)
+from smc.control.controller_templates.path_following_template import (
+    PathFollowingFromPlannerCtrllLoopTemplate,
+)
+
+import numpy as np
+from functools import partial
+import types
+from argparse import Namespace
+from pinocchio import SE3, log6
+from collections import deque
+
+
+def initializePastData(
+    args: Namespace, T_w_e: SE3, p_base: np.ndarray, max_base_v: float
+) -> np.ndarray:
+    # prepopulate past data to make base and cart be on the same path in the past
+    # (which didn't actually happen because this just started)
+    p_ee = T_w_e.translation[:2]
+    straight_line_path = np.linspace(p_ee, p_base, args.past_window_size)
+    # straight_line_path_timed = path2D_timed(args, straight_line_path, max_base_v)
+    # return straight_line_path_timed # this one is shortened to args.n_knots! and we want the whole buffer
+    return straight_line_path
+
+
+def SingleArmCartPullingMPCControlLoop(
+    ocp: CrocoOCP,
+    path2D_base: np.ndarray,
+    args: Namespace,
+    robot: SingleArmWholeBodyInterface,
+    t: int,
+    past_data: dict[str, deque[np.ndarray]],
+) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
+    p = robot.q[:2]
+
+    # NOTE: this one is obtained as the future path from path planner
+    max_base_v = np.linalg.norm(robot._max_v[:2])
+    trajectory_base = path2D_to_trajectory2D(args, path2D_base, max_base_v)
+    trajectory_base = np.hstack((trajectory_base, np.zeros((len(trajectory_base), 1))))
+
+    trajectorySE3_handlebar = construct_EE_path(args, p, past_data["path2D"])
+
+    if args.visualizer:
+        if t % int(np.ceil(args.ctrl_freq / 25)) == 0:
+            robot.visualizer_manager.sendCommand({"path": trajectory_base})
+            robot.visualizer_manager.sendCommand(
+                {"frame_path": trajectorySE3_handlebar}
+            )
+
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp.warmstartAndReSolve(x0, data=(trajectory_base, trajectorySE3_handlebar))
+    xs = np.array(ocp.solver.xs)
+    v_cmd = xs[1, robot.model.nq :]
+
+    err_vector_ee = log6(robot.T_w_e.actInv(trajectorySE3_handlebar[0]))
+    err_vector_base = np.linalg.norm(p - trajectory_base[0][:2])  # z axis is irrelevant
+    log_item = {}
+    log_item["err_vec_ee"] = err_vector_ee
+    log_item["err_norm_ee"] = np.linalg.norm(err_vector_ee).reshape((1,))
+    log_item["err_norm_base"] = np.linalg.norm(err_vector_base).reshape((1,))
+    save_past_item = {"path2D": p}
+    return v_cmd, save_past_item, log_item
+
+
+def SingleArmCartPullingMPC(
+    args: Namespace,
+    robot: SingleArmWholeBodyInterface,
+    path_planner: ProcessManager | types.FunctionType,
+    run=True,
+) -> None | ControlLoopManager:
+    """
+    BaseAndEEPathFollowingMPC
+    -----
+    run mpc for a point-to-point inverse kinematics.
+    note that the actual problem is solved on
+    a dynamics level, and velocities we command
+    are actually extracted from the state x(q,dq).
+    """
+
+    robot._mode = SingleArmWholeBodyInterface.control_mode.whole_body
+    T_w_e = robot.T_w_e
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp = BaseAndEEPathFollowingOCP(args, robot, x0)
+    ocp.solveInitialOCP(x0)
+
+    max_base_v = np.linalg.norm(robot._max_v[:2])
+
+    path2D_handlebar = initializePastData(args, T_w_e, robot.q[:2], float(max_base_v))
+
+    get_position = lambda robot: robot.q[:2]
+    controlLoop = partial(
+        PathFollowingFromPlannerCtrllLoopTemplate,
+        path_planner,
+        get_position,
+        ocp,
+        SingleArmCartPullingMPCControlLoop,
+        args,
+        robot,
+    )
+    log_item = {
+        "qs": np.zeros(robot.model.nq),
+        "dqs": np.zeros(robot.model.nv),
+        "err_vec_ee": np.zeros((6,)),
+        "err_norm_ee": np.zeros((1,)),
+        "err_norm_base": np.zeros((1,)),
+    }
+    save_past_dict = {"path2D": T_w_e.translation[:2]}
+    loop_manager = ControlLoopManager(
+        robot, controlLoop, args, save_past_dict, log_item
+    )
+
+    # actually put past data into the past window
+    loop_manager.past_data["path2D"].clear()
+    loop_manager.past_data["path2D"].extend(
+        path2D_handlebar[i] for i in range(args.past_window_size)
+    )
+
+    if run:
+        loop_manager.run()
+    else:
+        return loop_manager
diff --git a/examples/cart_pulling/control_sub_problems/with_planner/ee_only_path_following_from_plannner.py b/examples/cart_pulling/control_sub_problems/with_planner/ee_only_path_following_from_plannner.py
deleted file mode 100644
index 783743ab63ea13a1c2fe0c05610867a5a1ff60fc..0000000000000000000000000000000000000000
--- a/examples/cart_pulling/control_sub_problems/with_planner/ee_only_path_following_from_plannner.py
+++ /dev/null
@@ -1,88 +0,0 @@
-from smc import getRobotFromArgs
-from smc import getMinimalArgParser
-from smc.path_generation.maps.premade_maps import createSampleStaticMap
-from smc.path_generation.path_math.path2d_to_6d import path2D_to_SE3
-from smc.control.optimal_control.util import get_OCP_args
-from smc.control.cartesian_space import getClikArgs
-from smc.path_generation.planner import starPlanner, getPlanningArgs
-from smc.control.optimal_control.croco_mpc_point_to_point import CrocoEEP2PMPC
-from smc.control.optimal_control.croco_mpc_path_following import (
-    CrocoEEPathFollowingMPC,
-)
-from smc.multiprocessing import ProcessManager
-
-import time
-import numpy as np
-from functools import partial
-import pinocchio as pin
-
-
-def get_args():
-    parser = getMinimalArgParser()
-    parser = get_OCP_args(parser)
-    parser = getClikArgs(parser)  # literally just for goal error
-    parser = getPlanningArgs(parser)
-    parser.add_argument(
-        "--handlebar-height",
-        type=float,
-        default=0.5,
-        help="heigh of handlebar of the cart to be pulled",
-    )
-    args = parser.parse_args()
-    return args
-
-
-if __name__ == "__main__":
-    args = get_args()
-    robot = getRobotFromArgs(args)
-    # TODO: HOW IS IT POSSIBLE THAT T_W_E IS WRONG WITHOUT STEP CALLED HERE?????????????????
-    robot._step()
-    T_w_e = robot.T_w_e
-    robot._q[0] = 9.0
-    robot._q[1] = 4.0
-    robot._step()
-    x0 = np.concatenate([robot.q, robot.v])
-    goal = np.array([0.5, 5.5])
-
-    planning_function = partial(starPlanner, goal)
-    # here we're following T_w_e reference so that's what we send
-    path_planner = ProcessManager(
-        args, planning_function, T_w_e.translation[:2], 3, None
-    )
-    _, map_as_list = createSampleStaticMap()
-    if args.visualizer:
-        robot.sendRectangular2DMapToVisualizer(map_as_list)
-        # time.sleep(5)
-
-    T_w_e = robot.T_w_e
-    data = None
-    # get first path
-    while data is None:
-        path_planner.sendCommand(T_w_e.translation[:2])
-        data = path_planner.getData()
-        # time.sleep(1 / args.ctrl_freq)
-        time.sleep(1)
-
-    _, path2D = data
-    path2D = np.array(path2D).reshape((-1, 2))
-    pathSE3 = path2D_to_SE3(path2D, args.handlebar_height)
-    if args.visualizer:
-        # TODO: document this somewhere
-        robot.visualizer_manager.sendCommand({"Mgoal": pathSE3[0]})
-    if np.linalg.norm(pin.log6(T_w_e.actInv(pathSE3[0]))) > 1e-2:
-        print("going to initial path position")
-        CrocoEEP2PMPC(args, robot, pathSE3[0])
-    print("initialized!")
-    CrocoEEPathFollowingMPC(args, robot, x0, path_planner)
-
-    print("final position:", robot.T_w_e)
-
-    if args.real:
-        robot.stopRobot()
-
-    if args.save_log:
-        robot._log_manager.saveLog()
-        robot._log_manager.plotAllControlLoops()
-
-    if args.visualizer:
-        robot.killManipulatorVisualizer()
diff --git a/examples/optimal_control/path_following/base_and_ee_path_following.py b/examples/optimal_control/path_following/base_and_ee_path_following.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c6cc521c42791d166bb841ee3d977cfb2da8493
--- /dev/null
+++ b/examples/optimal_control/path_following/base_and_ee_path_following.py
@@ -0,0 +1,156 @@
+from smc import getRobotFromArgs
+from smc import getMinimalArgParser
+from smc.control.optimal_control.util import get_OCP_args
+from smc.control.cartesian_space import getClikArgs
+from smc.control.optimal_control.croco_mpc_path_following import (
+    BaseAndEEPathFollowingMPC,
+)
+from os.path import exists
+from smc import getRobotFromArgs
+from smc import getMinimalArgParser
+from smc.control.optimal_control.util import get_OCP_args
+from smc.control.cartesian_space import getClikArgs
+from smc.control.optimal_control.croco_mpc_point_to_point import (
+    CrocoEEAndBaseP2PMPC,
+)
+from smc.path_generation.planner import starPlanner, getPlanningArgs
+from smc.path_generation.maps.premade_maps import createSampleStaticMap
+from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface
+from smc.robots.interfaces.single_arm_interface import SingleArmInterface
+from smc.multiprocessing import ProcessManager
+
+from pinocchio import SE3
+import numpy as np
+from functools import partial
+import argparse
+import matplotlib
+from smc.util.draw_path import drawPath
+
+
+def get_args():
+    parser = getMinimalArgParser()
+    parser = get_OCP_args(parser)
+    parser = getClikArgs(parser)  # literally just for goal error
+    parser = getPlanningArgs(parser)
+    parser.add_argument(
+        "--planner",
+        action=argparse.BooleanOptionalAction,
+        help="if on, you're in a pre-set map and a planner produce a plan to navigate. if off, you draw the path to be followed",
+        default=True,
+    )
+    parser.add_argument(
+        "--draw-new",
+        action=argparse.BooleanOptionalAction,
+        help="are you drawing a new path or reusing the previous one",
+        default=False,
+    )
+    parser.add_argument(
+        "--map-width",
+        type=float,
+        help="width of the map in meters (x-axis) - only used for drawing of the path",
+        default=3.0,
+    )
+    parser.add_argument(
+        "--map-height",
+        type=float,
+        help="height of the map in meters (y-axis) - only used for drawing of the path",
+        default=3.0,
+    )
+    # TODO: rename argument here and in path following mpc
+    parser.add_argument(
+        "--handlebar-height",
+        type=float,
+        default=0.5,
+        help="heigh of handlebar of the cart to be pulled",
+    )
+    args = parser.parse_args()
+    return args
+
+
+# NOTE: lil bit of evil with passing the integer by reference -> make it a list
+# to make this non-evil this should be a method in a class but i ain't gonna bother with that now
+def fixedPathPlanner(
+    path_parameter: list[int],
+    path2D: np.ndarray,
+    rotation: np.ndarray,
+    base_offset: np.ndarray,
+    T_w_e: SE3,
+) -> tuple[list[SE3], list[np.ndarray]]:
+    p_ee = T_w_e.translation
+    distances = np.linalg.norm(p_ee - path2D, axis=1)
+    index = np.argmin(distances)
+    # if index > path_parameter[0]:
+    # NOTE: bullshit heuristic to deal with the path intersecting with itself.
+    # i can't be bothered with anything better
+    if (index - path_parameter[0]) > 0 and (index - path_parameter[0]) < 10:
+        path_parameter[0] += 1
+    path2D = path2D[path_parameter[0] :]
+    # NOTE: this is of course horribly inefficient,
+    # but since it's just for testing who cares
+    path_SE3 = []
+    path_base = []
+    for translation in path2D:
+        path_base.append(translation + base_offset)
+        path_SE3.append(SE3(rotation, translation))
+    return path_base, path_SE3
+
+
+if __name__ == "__main__":
+    args = get_args()
+    robot = getRobotFromArgs(args)
+    # TODO: make this go away
+    robot._step()
+    assert issubclass(robot.__class__, SingleArmInterface)
+    assert issubclass(robot.__class__, MobileBaseInterface)
+    x0 = np.concatenate([robot.q, robot.v])
+    rotation = SE3.Random().rotation
+    height = 0.5
+    base_offset = np.array([0.0, 0.4, 0.0])
+    goal = np.array([0.5, 5.5])
+    T_w_e = robot.T_w_e
+
+    if args.planner:
+        planning_function = partial(starPlanner, goal)
+        # here we're following T_w_e reference so that's what we send
+        path_planner = ProcessManager(
+            args, planning_function, T_w_e.translation[:2], 3, None
+        )
+        _, map_as_list = createSampleStaticMap()
+        if args.visualizer:
+            robot.sendRectangular2DMapToVisualizer(map_as_list)
+            # time.sleep(5)
+    else:
+        if not args.draw_new:
+            assert exists("./parameters/path_in_pixels.csv")
+            pixel_path_file_path = "./parameters/path_in_pixels.csv"
+            path2D = np.genfromtxt(pixel_path_file_path, delimiter=",")
+        else:
+            matplotlib.use("tkagg")
+            path2D = drawPath(args)
+            matplotlib.use("qtagg")
+        path2D[:, 0] = path2D[:, 0] * args.map_width
+        path2D[:, 1] = path2D[:, 1] * args.map_height
+        path2D = np.hstack((path2D, height * np.ones((len(path2D), 1))))
+        robot.updateViz({"fixed_path": path2D})
+        T_w_goal = SE3(rotation, path2D[0])
+        p_basegoal = T_w_goal.copy().translation.copy() + base_offset
+        p_basegoal[2] = 0.0
+        CrocoEEAndBaseP2PMPC(args, robot, T_w_goal, p_basegoal)
+        # note: making it a list is a lil'bit of evil to make it persistent in memory
+        # jesus i really need to switch to cpp to pass by reference or value at will
+        path_parameter = [0]
+        path_planner = partial(
+            fixedPathPlanner, path_parameter, path2D, rotation, base_offset
+        )
+
+    BaseAndEEPathFollowingMPC(args, robot, path_planner)
+
+    if args.real:
+        robot.stopRobot()
+
+    if args.save_log:
+        robot._log_manager.saveLog()
+        robot._log_manager.plotAllControlLoops()
+
+    if args.visualizer:
+        robot.killManipulatorVisualizer()
diff --git a/examples/optimal_control/path_following/croco_dual_ee_reference_path_following_mpc.py b/examples/optimal_control/path_following/croco_dual_ee_reference_path_following_mpc.py
new file mode 100644
index 0000000000000000000000000000000000000000..6917c4e0b3310c33d646a9522d0c065f2f48b0d1
--- /dev/null
+++ b/examples/optimal_control/path_following/croco_dual_ee_reference_path_following_mpc.py
@@ -0,0 +1,162 @@
+from os.path import exists
+from smc import getRobotFromArgs
+from smc import getMinimalArgParser
+from smc.control.optimal_control.util import get_OCP_args
+from smc.control.cartesian_space import getClikArgs
+from smc.control.cartesian_space.cartesian_space_point_to_point import moveLDualArm
+from smc.control.cartesian_space.pink_p2p import (
+    DualArmIKSelfAvoidanceViaEndEffectorSpheres,
+)
+from smc.control.optimal_control.croco_point_to_point.mpc.dual_arm_reference_mpc import (
+    CrocoDualEEP2PMPC,
+)
+from smc.control.optimal_control.croco_path_following.mpc.dual_arm_reference_mpc import (
+    CrocoDualArmEEPathFollowingMPC,
+)
+from smc.path_generation.planner import starPlanner, getPlanningArgs
+from smc.path_generation.maps.premade_maps import createSampleStaticMap
+from smc.robots.interfaces.dual_arm_interface import DualArmInterface
+from smc.multiprocessing import ProcessManager
+
+from pinocchio import SE3
+import numpy as np
+from functools import partial
+import argparse
+import matplotlib
+from smc.util.draw_path import drawPath
+
+
+def get_args():
+    parser = getMinimalArgParser()
+    parser = get_OCP_args(parser)
+    parser = getClikArgs(parser)  # literally just for goal error
+    parser = getPlanningArgs(parser)
+    parser.add_argument(
+        "--planner",
+        action=argparse.BooleanOptionalAction,
+        help="if on, you're in a pre-set map and a planner produce a plan to navigate. if off, you draw the path to be followed",
+        default=True,
+    )
+    parser.add_argument(
+        "--draw-new",
+        action=argparse.BooleanOptionalAction,
+        help="are you drawing a new path or reusing the previous one",
+        default=False,
+    )
+    parser.add_argument(
+        "--map-width",
+        type=float,
+        help="width of the map in meters (x-axis) - only used for drawing of the path",
+        default=3.0,
+    )
+    parser.add_argument(
+        "--map-height",
+        type=float,
+        help="height of the map in meters (y-axis) - only used for drawing of the path",
+        default=3.0,
+    )
+    # TODO: rename argument here and in path following mpc
+    parser.add_argument(
+        "--handlebar-height",
+        type=float,
+        default=0.5,
+        help="heigh of handlebar of the cart to be pulled",
+    )
+    args = parser.parse_args()
+    return args
+
+
+# NOTE: lil bit of evil with passing the integer by reference -> make it a list
+# to make this non-evil this should be a method in a class but i ain't gonna bother with that now
+def fixedPathPlanner(
+    path_parameter: list[int], path2D: np.ndarray, rotation: np.ndarray, T_w_e: SE3
+) -> list[SE3]:
+    p_ee = T_w_e.translation
+    distances = np.linalg.norm(p_ee - path2D, axis=1)
+    index = np.argmin(distances)
+    # if index > path_parameter[0]:
+    # NOTE: bullshit heuristic to deal with the path intersecting with itself.
+    # i can't be bothered with anything better
+    if (index - path_parameter[0]) > 0 and (index - path_parameter[0]) < 10:
+        path_parameter[0] += 1
+    path2D = path2D[path_parameter[0] :]
+    # NOTE: this is of course horribly inefficient,
+    # but since it's just for testing who cares
+    path_SE3 = []
+    for translation in path2D:
+        path_SE3.append(SE3(rotation, translation))
+    return path_SE3
+
+
+if __name__ == "__main__":
+    args = get_args()
+    robot = getRobotFromArgs(args)
+    # TODO: make this go away
+    robot._step()
+    assert issubclass(robot.__class__, DualArmInterface)
+    if args.planner:
+        robot._q[0] = 9.0
+        robot._q[1] = 4.0
+        robot._step()
+    x0 = np.concatenate([robot.q, robot.v])
+    # rotation = SE3.Random().rotation
+    rotation = SE3.Identity().rotation
+    height = 0.5
+    goal = np.array([0.5, 5.5])
+    T_w_abs = robot.T_w_abs
+    T_absgoal_l = SE3.Identity()
+    T_absgoal_l.translation[1] = 0.15
+    T_absgoal_r = SE3.Identity()
+    T_absgoal_r.translation[1] = -0.15
+
+    if args.planner:
+        planning_function = partial(starPlanner, goal)
+        # we define the path using the T_w_abs frame,
+        # from which the controller constructs T_w_l and T_w_r references to follow
+        path_planner = ProcessManager(
+            args, planning_function, T_w_abs.translation[:2], 3, None
+        )
+        _, map_as_list = createSampleStaticMap()
+        if args.visualizer:
+            robot.sendRectangular2DMapToVisualizer(map_as_list)
+            # time.sleep(5)
+    else:
+        if not args.draw_new:
+            assert exists("./parameters/path_in_pixels.csv")
+            pixel_path_file_path = "./parameters/path_in_pixels.csv"
+            path2D = np.genfromtxt(pixel_path_file_path, delimiter=",")
+        else:
+            matplotlib.use("tkagg")
+            path2D = drawPath(args)
+            matplotlib.use("qtagg")
+        path2D[:, 0] = path2D[:, 0] * args.map_width
+        path2D[:, 1] = path2D[:, 1] * args.map_height
+        path2D = np.hstack((path2D, height * np.ones((len(path2D), 1))))
+        robot.updateViz({"fixed_path": path2D})
+        T_w_absgoal = SE3(rotation, path2D[0])
+        moveLDualArm(args, robot, T_w_absgoal, T_absgoal_l, T_absgoal_r)
+        # and if that doesn't work either use pink:
+        # DualArmIKSelfAvoidanceViaEndEffectorSpheres(
+        #    T_w_absgoal, T_absgoal_l, T_absgoal_r, args, robot
+        # )
+        # NOTE: if this doesn't work just switch to moveL dual arm:
+        # CrocoDualEEP2PMPC(args, robot, T_w_goal, T_absgoal_l, T_absgoal_r)
+
+        # note: making it a list is a lil'bit of evil to make it persistent in memory
+        # jesus i really need to switch to cpp to pass by reference or value at will
+        path_parameter = [0]
+        path_planner = partial(fixedPathPlanner, path_parameter, path2D, rotation)
+
+    CrocoDualArmEEPathFollowingMPC(
+        args, robot, T_absgoal_l, T_absgoal_r, x0, path_planner
+    )
+
+    if args.real:
+        robot.stopRobot()
+
+    if args.save_log:
+        robot._log_manager.saveLog()
+        robot._log_manager.plotAllControlLoops()
+
+    if args.visualizer:
+        robot.killManipulatorVisualizer()
diff --git a/examples/optimal_control/path_following/croco_ee_reference_path_following_mpc.py b/examples/optimal_control/path_following/croco_ee_reference_path_following_mpc.py
new file mode 100644
index 0000000000000000000000000000000000000000..93f624e50e777e461f1d0599969c09167d6c33fc
--- /dev/null
+++ b/examples/optimal_control/path_following/croco_ee_reference_path_following_mpc.py
@@ -0,0 +1,143 @@
+from os.path import exists
+from smc import getRobotFromArgs
+from smc import getMinimalArgParser
+from smc.control.optimal_control.util import get_OCP_args
+from smc.control.cartesian_space import getClikArgs
+from smc.control.optimal_control.croco_point_to_point.mpc.single_arm_reference_mpc import (
+    CrocoEEP2PMPC,
+)
+from smc.control.optimal_control.croco_path_following.mpc.single_arm_reference_mpc import (
+    CrocoEEPathFollowingMPC,
+)
+from smc.path_generation.planner import starPlanner, getPlanningArgs
+from smc.path_generation.maps.premade_maps import createSampleStaticMap
+from smc.robots.interfaces.single_arm_interface import SingleArmInterface
+from smc.multiprocessing import ProcessManager
+
+from pinocchio import SE3
+import numpy as np
+from functools import partial
+import argparse
+import matplotlib
+from smc.util.draw_path import drawPath
+
+
+def get_args():
+    parser = getMinimalArgParser()
+    parser = get_OCP_args(parser)
+    parser = getClikArgs(parser)  # literally just for goal error
+    parser = getPlanningArgs(parser)
+    parser.add_argument(
+        "--planner",
+        action=argparse.BooleanOptionalAction,
+        help="if on, you're in a pre-set map and a planner produce a plan to navigate. if off, you draw the path to be followed",
+        default=True,
+    )
+    parser.add_argument(
+        "--draw-new",
+        action=argparse.BooleanOptionalAction,
+        help="are you drawing a new path or reusing the previous one",
+        default=False,
+    )
+    parser.add_argument(
+        "--map-width",
+        type=float,
+        help="width of the map in meters (x-axis) - only used for drawing of the path",
+        default=3.0,
+    )
+    parser.add_argument(
+        "--map-height",
+        type=float,
+        help="height of the map in meters (y-axis) - only used for drawing of the path",
+        default=3.0,
+    )
+    # TODO: rename argument here and in path following mpc
+    parser.add_argument(
+        "--handlebar-height",
+        type=float,
+        default=0.5,
+        help="heigh of handlebar of the cart to be pulled",
+    )
+    args = parser.parse_args()
+    return args
+
+
+# NOTE: lil bit of evil with passing the integer by reference -> make it a list
+# to make this non-evil this should be a method in a class but i ain't gonna bother with that now
+def fixedPathPlanner(
+    path_parameter: list[int], path2D: np.ndarray, rotation: np.ndarray, T_w_e: SE3
+) -> list[SE3]:
+    p_ee = T_w_e.translation
+    distances = np.linalg.norm(p_ee - path2D, axis=1)
+    index = np.argmin(distances)
+    # if index > path_parameter[0]:
+    # NOTE: bullshit heuristic to deal with the path intersecting with itself.
+    # i can't be bothered with anything better
+    if (index - path_parameter[0]) > 0 and (index - path_parameter[0]) < 10:
+        path_parameter[0] += 1
+    path2D = path2D[path_parameter[0] :]
+    # NOTE: this is of course horribly inefficient,
+    # but since it's just for testing who cares
+    path_SE3 = []
+    for translation in path2D:
+        path_SE3.append(SE3(rotation, translation))
+    return path_SE3
+
+
+if __name__ == "__main__":
+    args = get_args()
+    robot = getRobotFromArgs(args)
+    # TODO: make this go away
+    robot._step()
+    assert issubclass(robot.__class__, SingleArmInterface)
+    if args.planner:
+        robot._q[0] = 9.0
+        robot._q[1] = 4.0
+        robot._step()
+    x0 = np.concatenate([robot.q, robot.v])
+    rotation = SE3.Random().rotation
+    height = 0.5
+    goal = np.array([0.5, 5.5])
+    T_w_e = robot.T_w_e
+
+    if args.planner:
+        planning_function = partial(starPlanner, goal)
+        # here we're following T_w_e reference so that's what we send
+        path_planner = ProcessManager(
+            args, planning_function, T_w_e.translation[:2], 3, None
+        )
+        _, map_as_list = createSampleStaticMap()
+        if args.visualizer:
+            robot.sendRectangular2DMapToVisualizer(map_as_list)
+            # time.sleep(5)
+    else:
+        if not args.draw_new:
+            assert exists("./parameters/path_in_pixels.csv")
+            pixel_path_file_path = "./parameters/path_in_pixels.csv"
+            path2D = np.genfromtxt(pixel_path_file_path, delimiter=",")
+        else:
+            matplotlib.use("tkagg")
+            path2D = drawPath(args)
+            matplotlib.use("qtagg")
+        path2D[:, 0] = path2D[:, 0] * args.map_width
+        path2D[:, 1] = path2D[:, 1] * args.map_height
+        path2D = np.hstack((path2D, height * np.ones((len(path2D), 1))))
+        robot.updateViz({"fixed_path": path2D})
+        T_w_goal = SE3(rotation, path2D[0])
+        CrocoEEP2PMPC(args, robot, T_w_goal)
+        # note: making it a list is a lil'bit of evil to make it persistent in memory
+        # jesus i really need to switch to cpp to pass by reference or value at will
+        path_parameter = [0]
+        path_planner = partial(fixedPathPlanner, path_parameter, path2D, rotation)
+
+    CrocoEEPathFollowingMPC(args, robot, x0, path_planner)
+
+    if args.real:
+        robot.stopRobot()
+
+    if args.save_log:
+        robot._log_manager.saveLog()
+        robot._log_manager.plotAllControlLoops()
+
+    if args.visualizer:
+        robot.killManipulatorVisualizer()
diff --git a/examples/optimal_control/path_following/path_following_mpc.py b/examples/optimal_control/path_following/path_following_mpc.py
deleted file mode 100644
index 5ec64e77586c793bed74bbef2ee98c3cf54ef8ae..0000000000000000000000000000000000000000
--- a/examples/optimal_control/path_following/path_following_mpc.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from smc import getRobotFromArgs
-from smc import getMinimalArgParser
-from smc.control.optimal_control.util import get_OCP_args
-from smc.control.cartesian_space import getClikArgs
-from smc.control.optimal_control.croco_mpc_path_following import (
-    CrocoEEPathFollowingMPC,
-)
-import numpy as np
-
-
-def path(T_w_e, i):
-    # 2) do T_mobile_base_ee_pos to get
-    # end-effector reference frame(s)
-
-    # generate bullshit just to see it works
-    path = []
-    t = i * robot.dt
-    for i in range(args.n_knots):
-        t += 0.01
-        new = T_w_e.copy()
-        translation = 2 * np.array([np.cos(t / 20), np.sin(t / 20), 0.3])
-        new.translation = translation
-        path.append(new)
-    return path
-
-
-def get_args():
-    parser = getMinimalArgParser()
-    parser = get_OCP_args(parser)
-    parser = getClikArgs(parser)  # literally just for goal error
-    args = parser.parse_args()
-    return args
-
-
-if __name__ == "__main__":
-    args = get_args()
-    robot = getRobotFromArgs(args)
-    x0 = np.concatenate([robot.q, robot.v])
-    robot._step()
-
-    CrocoEEPathFollowingMPC(args, robot, x0, path)
-
-    if args.real:
-        robot.stopRobot()
-
-    if args.save_log:
-        robot._log_manager.saveLog()
-        robot._log_manager.plotAllControlLoops()
-
-    if args.visualizer:
-        robot.killManipulatorVisualizer()
diff --git a/examples/optimal_control/path_following/test_by_running.sh b/examples/optimal_control/path_following/test_by_running.sh
new file mode 100755
index 0000000000000000000000000000000000000000..69cf6d2fa8ac5d401d84a198ec5f64c8985b727d
--- /dev/null
+++ b/examples/optimal_control/path_following/test_by_running.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+# the idea here is to run all the runnable things
+# and test for syntax errors 
+# TODO: make these work with different modes by making robot.model a property which outputs truncated models
+
+##########################################################################################################
+#                                         fixed path
+# ################################################################################################
+# single arm - ee reference
+# ###############
+runnable="croco_ee_reference_path_following_mpc.py --ctrl-freq=-1 --no-plotter --no-visualizer --max-iterations=2 --no-draw-new --no-planner"
+echo $runnable
+python $runnable
+
+
+####################################################################################
+#                                path from planner
+#################################################################33
+
+runnable="croco_ee_reference_path_following_mpc.py --ctrl-freq=-1 --no-plotter --no-visualizer --max-iterations=2 --no-draw-new --planner"
+echo $runnable
+python $runnable
diff --git a/examples/optimal_control/point_to_point/croco_base_and_dual_ee_reference_p2p.py b/examples/optimal_control/point_to_point/croco_base_and_dual_ee_reference_p2p.py
index 1f1fa1bb4734f9c625b469ea9e3a4d469daf8108..765206e52fa67e3997dd0653693ddadef44a5c4a 100644
--- a/examples/optimal_control/point_to_point/croco_base_and_dual_ee_reference_p2p.py
+++ b/examples/optimal_control/point_to_point/croco_base_and_dual_ee_reference_p2p.py
@@ -2,7 +2,7 @@ from smc import getRobotFromArgs
 from smc import getMinimalArgParser
 from smc.control.optimal_control.util import get_OCP_args
 from smc.control.cartesian_space import getClikArgs
-from smc.control.optimal_control.croco_mpc_point_to_point import (
+from smc.control.optimal_control.croco_point_to_point.mpc.base_and_dual_arm_reference_mpc import (
     CrocoDualEEAndBaseP2PMPC,
 )
 from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface
diff --git a/examples/optimal_control/point_to_point/croco_base_and_ee_reference_p2p.py b/examples/optimal_control/point_to_point/croco_base_and_ee_reference_p2p.py
index 8fd3c4c0e98dc6e383d9a30d910ce39c8430c98e..a8321b436c03f390b3260a4d74e733930530c314 100644
--- a/examples/optimal_control/point_to_point/croco_base_and_ee_reference_p2p.py
+++ b/examples/optimal_control/point_to_point/croco_base_and_ee_reference_p2p.py
@@ -2,7 +2,9 @@ from smc import getRobotFromArgs
 from smc import getMinimalArgParser
 from smc.control.optimal_control.util import get_OCP_args
 from smc.control.cartesian_space import getClikArgs
-from smc.control.optimal_control.croco_mpc_point_to_point import CrocoEEAndBaseP2PMPC
+from smc.control.optimal_control.croco_point_to_point.mpc.base_and_single_arm_reference_mpc import (
+    CrocoEEAndBaseP2PMPC,
+)
 from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface
 from smc.robots.interfaces.single_arm_interface import SingleArmInterface
 
diff --git a/examples/optimal_control/point_to_point/croco_dual_ee_reference_p2p_mpc.py b/examples/optimal_control/point_to_point/croco_dual_ee_reference_p2p_mpc.py
index 82a3cc27a7b348efc16945186a6dda93f156a528..35b0f7e067f5dacd64719b518666ca53fa5d9bd2 100644
--- a/examples/optimal_control/point_to_point/croco_dual_ee_reference_p2p_mpc.py
+++ b/examples/optimal_control/point_to_point/croco_dual_ee_reference_p2p_mpc.py
@@ -1,5 +1,5 @@
 from smc import getMinimalArgParser, getRobotFromArgs
-from smc.control.optimal_control.croco_mpc_point_to_point import (
+from smc.control.optimal_control.croco_point_to_point.mpc.dual_arm_reference_mpc import (
     CrocoDualEEP2PMPC,
 )
 from smc.control.optimal_control.util import get_OCP_args
diff --git a/examples/optimal_control/point_to_point/croco_ee_reference_p2p_mpc.py b/examples/optimal_control/point_to_point/croco_ee_reference_p2p_mpc.py
index 0b21cd09225fbb2edf85f968b3c938c72e797ede..92f88775e3ab76078bf4430114c3061d614f4184 100644
--- a/examples/optimal_control/point_to_point/croco_ee_reference_p2p_mpc.py
+++ b/examples/optimal_control/point_to_point/croco_ee_reference_p2p_mpc.py
@@ -1,5 +1,5 @@
 from smc import getMinimalArgParser, getRobotFromArgs
-from smc.control.optimal_control.croco_mpc_point_to_point import (
+from smc.control.optimal_control.croco_point_to_point.mpc.single_arm_reference_mpc import (
     CrocoEEP2PMPC,
 )
 from smc.control.optimal_control.util import get_OCP_args
diff --git a/examples/optimal_control/point_to_point/croco_ee_reference_p2p_ocp.py b/examples/optimal_control/point_to_point/croco_ee_reference_p2p_ocp.py
index 0b85c48edf9ed32152d87b1bd8f3647a0ed833a9..907ef7a9e8b4c9fb6b731611fc300d2fd59e5fc2 100644
--- a/examples/optimal_control/point_to_point/croco_ee_reference_p2p_ocp.py
+++ b/examples/optimal_control/point_to_point/croco_ee_reference_p2p_ocp.py
@@ -1,7 +1,7 @@
 # PYTHON_ARGCOMPLETE_OK
 from smc.control.optimal_control.util import get_OCP_args
 from smc import getMinimalArgParser, getRobotFromArgs
-from smc.control.optimal_control.point_to_point_croco_ocp import (
+from smc.control.optimal_control.croco_point_to_point.ocp.single_arm_reference_ocp import (
     SingleArmIKOCP,
 )
 from smc.control.joint_space import followKinematicJointTrajP
diff --git a/python/smc/control/cartesian_space/cartesian_space_point_to_point.py b/python/smc/control/cartesian_space/cartesian_space_point_to_point.py
index 7b52ccd3af6be7c1d482edd1c2e80d0ee5ba4939..8b71f35dd224d629eeeaeee5dfb72068d7d41ba0 100644
--- a/python/smc/control/cartesian_space/cartesian_space_point_to_point.py
+++ b/python/smc/control/cartesian_space/cartesian_space_point_to_point.py
@@ -16,6 +16,8 @@ from argparse import Namespace
 from collections import deque
 from typing import Callable
 
+from smc.control.cartesian_space.ik_solvers import QPManipMax
+
 
 def controlLoopClik(
     #                       J           err_vec     v_cmd
@@ -39,7 +41,16 @@ def controlLoopClik(
     J = robot.getJacobian()
     # compute the joint velocities based on controller you passed
     # qd = ik_solver(J, err_vector, past_qd=past_data['dqs_cmd'][-1])
-    v_cmd = ik_solver(J, err_vector)
+    if args.ik_solver == "QPManipMax":
+        v_cmd = QPManipMax(
+            J,
+            err_vector,
+            robot.computeManipulabilityIndexQDerivative(),
+            lb=-1 * robot.max_v,
+            ub=robot.max_v,
+        )
+    else:
+        v_cmd = ik_solver(J, err_vector)
     if v_cmd is None:
         print(
             t,
@@ -202,7 +213,17 @@ def controlLoopClikDualArm(
 
     err_vector = np.concatenate((err_vector_left, err_vector_right))
     J = robot.getJacobian()
-    v_cmd = ik_solver(J, err_vector)
+
+    if args.ik_solver == "QPManipMax":
+        v_cmd = QPManipMax(
+            J,
+            err_vector,
+            robot.computeManipulabilityIndexQDerivative(),
+            lb=-1 * robot.max_v,
+            ub=robot.max_v,
+        )
+    else:
+        v_cmd = ik_solver(J, err_vector)
     if v_cmd is None:
         print(
             t,
diff --git a/python/smc/control/cartesian_space/ik_solvers.py b/python/smc/control/cartesian_space/ik_solvers.py
index a24e140df966434b97797d7479e4da77e4d98776..f933638b0a50dff4a7c3ba7d98592662ec46128d 100644
--- a/python/smc/control/cartesian_space/ik_solvers.py
+++ b/python/smc/control/cartesian_space/ik_solvers.py
@@ -40,6 +40,11 @@ def getIKSolver(
         lb = -1 * robot.max_v
         ub = robot.max_v
         return partial(QPquadprog, lb=lb, ub=ub)
+    # via quadprog
+    #    if args.ik_solver == "QPManipMax":
+    #        lb = -1 * robot.max_v
+    #        ub = robot.max_v
+    #        return partial(QPManipMax, lb=lb, ub=ub)
     if args.ik_solver == "QPproxsuite":
         H = np.eye(robot.nv)
         g = np.zeros(robot.nv)
@@ -211,26 +216,38 @@ def QPproxsuite(
 # HA! found it in a winter school
 # use this
 # and test it with finite differencing!
-"""
 class CostManipulability:
-    def __init__(self,jointIndex=None,frameIndex=None):
+    def __init__(self, jointIndex=None, frameIndex=None):
         if frameIndex is not None:
             jointIndex = robot.model.frames[frameIndex].parent
-        self.jointIndex = jointIndex if jointIndex is not None else robot.model.njoints-1
-    def calc(self,q):
-        J = self.J=pin.computeJointJacobian(robot.model,robot.data,q,self.jointIndex)
-        return np.sqrt(det(J@J.T))
-    def calcDiff(self,q):
-        Jp = pinv(pin.computeJointJacobian(robot.model,robot.data,q,self.jointIndex))
+        self.jointIndex = (
+            jointIndex if jointIndex is not None else robot.model.njoints - 1
+        )
+
+    def calc(self, q):
+        J = self.J = pin.computeJointJacobian(
+            robot.model, robot.data, q, self.jointIndex
+        )
+        return np.sqrt(det(J @ J.T))
+
+    def calcDiff(self, q):
+        Jp = pinv(pin.computeJointJacobian(robot.model, robot.data, q, self.jointIndex))
         res = np.zeros(robot.model.nv)
         v0 = np.zeros(robot.model.nv)
         for k in range(6):
-            pin.computeForwardKinematicsDerivatives(robot.model,robot.data,q,Jp[:,k],v0)
-            JqJpk = pin.getJointVelocityDerivatives(robot.model,robot.data,self.jointIndex,pin.LOCAL)[0]
-            res += JqJpk[k,:]
+            pin.computeForwardKinematicsDerivatives(
+                robot.model, robot.data, q, Jp[:, k], v0
+            )
+            JqJpk = pin.getJointVelocityDerivatives(
+                robot.model, robot.data, self.jointIndex, pin.LOCAL
+            )[0]
+            res += JqJpk[k, :]
         res *= self.calc(q)
         return res
 
+
+"""
+
 # use this as a starting point for finite differencing
 def numdiff(func, x, eps=1e-6):
     f0 = copy.copy(func(x))
@@ -255,9 +272,15 @@ Tgn = Tdiffq(costManipulability.calc,q)
 """
 
 
-def QPManipMax(J: np.ndarray, err_vector: np.ndarray, lb=None, ub=None) -> np.ndarray:
+def QPManipMax(
+    J: np.ndarray,
+    V_e_e: np.ndarray,
+    secondary_objective_vec: np.ndarray,
+    lb=None,
+    ub=None,
+) -> np.ndarray:
     """
-    invKinmQP
+    QPManipMAx
     ---------
     generic QP:
         minimize 1/2 x^T P x + q^T x
@@ -267,7 +290,7 @@ def QPManipMax(J: np.ndarray, err_vector: np.ndarray, lb=None, ub=None) -> np.nd
                  lb <= x <= ub
     inverse kinematics QP:
         minimize 1/2 qd^T P qd
-                    + q^T qd (optional secondary objective)
+                    + q^T qd (where q is the partial deriviative of the manipulability index w.r.t. q)
         subject to
                  G qd \\leq h (optional)
                  J qd = b    (mandatory)
@@ -276,14 +299,15 @@ def QPManipMax(J: np.ndarray, err_vector: np.ndarray, lb=None, ub=None) -> np.nd
     P = np.eye(J.shape[1], dtype="double")
     # secondary objective is given via q
     # we set it to 0 here, but we should give a sane default here
-    q = np.array([0] * J.shape[1], dtype="double")
+    q = secondary_objective_vec
     G = None
-    b = err_vector
+    V_e_e_norm = np.linalg.norm(V_e_e)
+    max_V_e_e_norm = 0.3
+    if V_e_e_norm < max_V_e_e_norm:
+        b = V_e_e
+    else:
+        b = (V_e_e / V_e_e_norm) * max_V_e_e_norm
     A = J
-    # TODO: you probably want limits here
-    # lb = None
-    # ub = None
     h = None
-    # qd = solve_qp(P, q, G, h, A, b, lb, ub, solver="ecos")
-    qd = solve_qp(P, q, G, h, A, b, lb, ub, solver="quadprog")
+    qd = solve_qp(P, q, G, h, A, b, lb, ub, solver="quadprog", verbose=False)
     return qd
diff --git a/python/smc/control/cartesian_space/utils.py b/python/smc/control/cartesian_space/utils.py
index 685ed51b98af585f76964fdc5efebb92ea28c5c9..d98d519f0b9c03837ce45cb6892acd60ef2261cc 100644
--- a/python/smc/control/cartesian_space/utils.py
+++ b/python/smc/control/cartesian_space/utils.py
@@ -46,6 +46,7 @@ def getClikArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
             "jacobianTranspose",
             "QPquadprog",
             "QPproxsuite",
+            "QPManipMax",
         ],
     )
 
diff --git a/python/smc/control/controller_templates/point_to_point.py b/python/smc/control/controller_templates/point_to_point.py
index 96e334e7f57214ee60136311428c0402011be31f..04d57137387267325676ff10ec4a0acaa7967b40 100644
--- a/python/smc/control/controller_templates/point_to_point.py
+++ b/python/smc/control/controller_templates/point_to_point.py
@@ -215,7 +215,7 @@ def EEAndBaseP2PCtrlLoopTemplate(
     ee_err_vector = log6(SEerror).vector
     ee_err_vector_norm = np.linalg.norm(ee_err_vector)
 
-    p_base = np.array(list(robot.q[:2]) + [0.0])
+    p_base = robot.T_w_b.translation
     base_err_vector_norm = np.linalg.norm(p_basegoal - p_base)
 
     if (ee_err_vector_norm < robot.args.goal_error) and (
diff --git a/python/smc/control/optimal_control/__init__.py b/python/smc/control/optimal_control/__init__.py
index bd3883b08e418e5c1f5c1efb957bd0d1189b5dbc..e121f5ab4f58acf9fafc15611c2c952d5edd1983 100644
--- a/python/smc/control/optimal_control/__init__.py
+++ b/python/smc/control/optimal_control/__init__.py
@@ -6,7 +6,5 @@ import importlib.util
 #        print("you need to have pinocchio version 3.0.0 or greater to use pinocchio.casadi!")
 #        exit()
 #    from .create_pinocchio_casadi_ocp import *
-from .path_following_croco_ocp import *
-from .point_to_point_croco_ocp import *
-from .croco_mpc_path_following import *
-from .croco_mpc_point_to_point import *
+from .croco_path_following import *
+from .croco_point_to_point import *
diff --git a/python/smc/control/optimal_control/croco_mpc_path_following.py b/python/smc/control/optimal_control/croco_mpc_path_following.py
deleted file mode 100644
index 027ace12b08cd5d16748bfea4bb21377685905bd..0000000000000000000000000000000000000000
--- a/python/smc/control/optimal_control/croco_mpc_path_following.py
+++ /dev/null
@@ -1,534 +0,0 @@
-from smc.robots.interfaces.single_arm_interface import SingleArmInterface
-from smc.robots.interfaces.whole_body_interface import SingleArmWholeBodyInterface, DualArmWholeBodyInterface
-from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface
-from smc.control.control_loop_manager import ControlLoopManager
-from smc.multiprocessing.process_manager import ProcessManager
-from smc.control.optimal_control.abstract_croco_ocp import CrocoOCP
-from smc.control.optimal_control.path_following_croco_ocp import (
-    BasePathFollowingOCP,
-    CrocoEEPathFollowingOCP,
-    BaseAndEEPathFollowingOCP,
-    BaseAndDualArmEEPathFollowingOCP,
-)
-from smc.path_generation.path_math.path2d_to_6d import (
-    path2D_to_SE3,
-)
-from smc.path_generation.path_math.cart_pulling_path_math import (
-    construct_EE_path,
-)
-from smc.path_generation.path_math.path_to_trajectory import path2D_to_trajectory2D
-from smc.control.controller_templates.path_following_template import (
-    PathFollowingFromPlannerCtrllLoopTemplate,
-)
-
-import numpy as np
-from functools import partial
-import types
-from argparse import Namespace
-from pinocchio import SE3, log6
-from collections import deque
-
-
-def CrocoBasePathFollowingFromPlannerMPCControlLoop(
-    ocp: CrocoOCP,
-    path2D: np.ndarray,
-    args: Namespace,
-    robot: MobileBaseInterface,
-    t: int,
-    _: dict[str, deque[np.ndarray]],
-) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
-
-    p = robot.T_w_b.translation[:2]
-    max_base_v = np.linalg.norm(robot._max_v[:2])
-    path_base = path2D_to_trajectory2D(args, path2D, max_base_v)
-    path_base = np.hstack((path_base, np.zeros((len(path_base), 1))))
-
-    if args.visualizer:
-        if t % int(np.ceil(args.ctrl_freq / 25)) == 0:
-            robot.visualizer_manager.sendCommand({"path": path_base})
-
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp.warmstartAndReSolve(x0, data=(path_base))
-    xs = np.array(ocp.solver.xs)
-    v_cmd = xs[1, robot.model.nq :]
-    # NOTE: we might get stuck
-    # TODO: make it more robust, it can still get stuck with this
-    # a good idea is to do this for a some number of iterations - 
-    # you can keep this them in past data
-    if np.linalg.norm(v_cmd) < 0.05:
-        print(t, "RESOLVING FOR ONLY FINAL PATH POINT")
-        last_point_only = np.ones((len(path_base),2))
-        last_point_only =  np.hstack((last_point_only, np.zeros((len(path_base),1))))
-        last_point_only = last_point_only * path_base[-1]
-        ocp.warmstartAndReSolve(x0, data=(last_point_only))
-        xs = np.array(ocp.solver.xs)
-        v_cmd = xs[1, robot.model.nq :]
-
-    err_vector_base = np.linalg.norm(p - path_base[0][:2])  # z axis is irrelevant
-    log_item = {}
-    log_item["err_norm_base"] = np.linalg.norm(err_vector_base).reshape((1,))
-    return v_cmd, {}, log_item
-
-
-def CrocoBasePathFollowingMPC(
-    args: Namespace,
-    robot: MobileBaseInterface,
-    x0: np.ndarray,
-    path_planner: ProcessManager | types.FunctionType,
-run=True) -> None|ControlLoopManager:
-    """
-    CrocoBasePathFollowingMPC
-    -----
-    """
-
-    ocp = BasePathFollowingOCP(args, robot, x0)
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp.solveInitialOCP(x0)
-
-    get_position = lambda robot: robot.T_w_b.translation[:2]
-    controlLoop = partial(
-        PathFollowingFromPlannerCtrllLoopTemplate,
-        path_planner,
-        get_position,
-        ocp,
-        CrocoBasePathFollowingFromPlannerMPCControlLoop,
-        args,
-        robot,
-    )
-    log_item = {
-        "qs": np.zeros(robot.nq),
-        "dqs": np.zeros(robot.nv),
-        "err_norm_base": np.zeros((1,)),
-    }
-    save_past_item = {}
-    loop_manager = ControlLoopManager(
-        robot, controlLoop, args, save_past_item, log_item
-    )
-    if run:
-        loop_manager.run()
-    else:
-        return loop_manager
-
-
-# NOTE: DEPRICATED WILL GET DELETED SOON (TEMPLATE HANDLES IT NOW)
-def CrocoEEPathFollowingMPCControlLoop(
-    args: Namespace,
-    robot: SingleArmInterface,
-    ocp: CrocoOCP,
-    path_planner: types.FunctionType,
-    t: int,
-    _: dict[str, deque[np.ndarray]],
-) -> tuple[np.ndarray, dict[str, np.ndarray],dict[str, np.ndarray]]:
-    """
-    CrocoPathFollowingMPCControlLoop
-    -----------------------------
-    end-effector(s) follow their path(s).
-
-    path planner can either be a function which spits out a list of path points
-    or an instance of ProcessManager which spits out path points
-    by calling ProcessManager.getData()
-    """
-    breakFlag = False
-    log_item = {}
-    save_past_item = {}
-
-    q = robot.q
-    T_w_e = robot.T_w_e
-
-    path_EE_SE3 = path_planner(T_w_e, t)
-
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp.warmstartAndReSolve(x0, data=path_EE_SE3)
-    xs = np.array(ocp.solver.xs)
-    us = np.array(ocp.solver.us)
-    vel_cmds = xs[1, robot.model.nq :]
-
-    robot.sendVelocityCommand(vel_cmds)
-
-    # TODO: EVIL AND HAS TO BE REMOVED FROM HERE
-    if args.visualizer:
-        if t % int(np.ceil(args.ctrl_freq / 25)) == 0:
-            robot.visualizer_manager.sendCommand({"frame_path": path_EE_SE3})
-
-    err_vector_ee = log6(robot.T_w_e.actInv(path_EE_SE3[0]))
-    log_item["err_vec_ee"] = err_vector_ee
-    log_item["qs"] = q.reshape((robot.model.nq,))
-    log_item["dqs"] = robot.v.reshape((robot.model.nv,))
-    return breakFlag, save_past_item, log_item
-
-
-def CrocoEEPathFollowingFromPlannerMPCControlLoop(
-    ocp: CrocoOCP,
-    path2D: np.ndarray,
-    args: Namespace,
-    robot: SingleArmInterface,
-    t: int,
-    _: dict[str, deque[np.ndarray]],
-) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
-    """
-    CrocoPathFollowingMPCControlLoop
-    -----------------------------
-    end-effector(s) follow their path(s).
-
-    path planner can either be a function which spits out a list of path points
-    or an instance of ProcessManager which spits out path points
-    by calling ProcessManager.getData()
-    """
-    max_base_v = np.linalg.norm(robot._max_v[:2])
-    perc_of_max_v = 0.5
-    velocity = perc_of_max_v * max_base_v
-    trajectory2D = path2D_to_trajectory2D(args, path2D, velocity)
-
-    # create a 3D reference out of the path
-    path_EE_SE3 = path2D_to_SE3(trajectory2D, args.handlebar_height)
-    #    for pose in path_EE_SE3:
-    #        print(pose.translation)
-
-    # TODO: EVIL AND HAS TO BE REMOVED FROM HERE
-    if args.visualizer:
-        if t % int(np.ceil(args.ctrl_freq / 25)) == 0:
-            robot.visualizer_manager.sendCommand({"frame_path": path_EE_SE3})
-
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp.warmstartAndReSolve(x0, data=path_EE_SE3)
-    xs = np.array(ocp.solver.xs)
-    v_cmd = xs[1, robot.model.nq :]
-
-    err_vector_ee = log6(robot.T_w_e.actInv(path_EE_SE3[0])).vector
-    log_item = {"err_vec_ee": err_vector_ee}
-
-    return v_cmd, {}, log_item
-
-
-def CrocoEEPathFollowingMPC(
-    args: Namespace,
-    robot: SingleArmInterface,
-    x0: np.ndarray,
-    path_planner: ProcessManager | types.FunctionType,
-run=True) -> None|ControlLoopManager:
-    """
-    CrocoEndEffectorPathFollowingMPC
-    -----
-    follow a fixed pre-determined path, or a path received from a planner.
-    the path does NOT need to start from your current pose - you need to get to it yourself.
-    """
-
-    ocp = CrocoEEPathFollowingOCP(args, robot, x0)
-    # technically should be done in controlloop because now
-    # it's solved 2 times before the first command,
-    # but we don't have time for details rn
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp.solveInitialOCP(x0)
-
-    if type(path_planner) == types.FunctionType:
-        controlLoop = partial(
-            # NOTE: DEPRICATED WILL GET DELETED SOON (TEMPLATE HANDLES IT NOW)
-            CrocoEEPathFollowingMPCControlLoop, args, robot, ocp, path_planner
-        )
-    else:
-        get_position = lambda robot: robot.T_w_e.translation[:2]
-        controlLoop = partial(
-            PathFollowingFromPlannerCtrllLoopTemplate,
-            path_planner,
-            get_position,
-            ocp,
-            CrocoEEPathFollowingFromPlannerMPCControlLoop,
-            args,
-            robot,
-        )
-    log_item = {
-        "qs": np.zeros(robot.model.nq),
-        "dqs": np.zeros(robot.model.nv),
-        "err_vec_ee": np.zeros(6),
-    }
-    save_past_item = {}
-    loop_manager = ControlLoopManager(
-        robot, controlLoop, args, save_past_item, log_item
-    )
-    if run:
-        loop_manager.run()
-    else:
-        return loop_manager
-
-# NOTE: DEPRICATED WILL GET DELETED SOON (TEMPLATE HANDLES IT NOW)
-def BaseAndEEPathFollowingMPCControlLoop(
-    args: Namespace,
-    robot,
-    ocp: CrocoOCP,
-    path_planner: types.FunctionType,
-    t: int,
-    _: dict[str, deque[np.ndarray]],
-) -> tuple[np.ndarray, dict[str, np.ndarray]]:
-    """
-    CrocoPathFollowingMPCControlLoop
-    -----------------------------
-    end-effector(s) follow their path(s).
-
-    path planner can either be a function which spits out a list of path points
-    or an instance of ProcessManager which spits out path points
-    by calling ProcessManager.getData()
-    """
-    breakFlag = False
-    log_item = {}
-    save_past_dict = {}
-
-    q = robot.q
-    T_w_e = robot.T_w_e
-    path_base, path_EE = path_planner(T_w_e, t)
-
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp.warmstartAndReSolve(x0, data=(path_base, path_EE))
-    xs = np.array(ocp.solver.xs)
-    us = np.array(ocp.solver.us)
-    vel_cmds = xs[1, robot.model.nq :]
-
-    robot.sendVelocityCommand(vel_cmds)
-    if t % int(np.ceil(args.ctrl_freq / 25)) == 0:
-        path_base = np.array(path_base)
-        robot.visualizer_manager.sendCommand({"path": path_base})
-        robot.visualizer_manager.sendCommand({"frame_path": path_EE})
-
-    err_vector_ee = log6(T_w_e.actInv(path_EE[0]))
-    err_vector_base = np.linalg.norm(q[:2] - path_base[0][:2])  # z axis is irrelevant
-    log_item["err_vec_ee"] = err_vector_ee
-    log_item["err_norm_ee"] = np.linalg.norm(err_vector_ee).reshape((1,))
-    log_item["err_norm_base"] = np.linalg.norm(err_vector_base).reshape((1,))
-    log_item["qs"] = q.reshape((robot.model.nq,))
-    log_item["dqs"] = robot.v.reshape((robot.model.nv,))
-    return breakFlag, save_past_dict, log_item
-
-
-def BaseAndEEPathFollowingFromPlannerMPCControlLoop(
-    ocp: CrocoOCP,
-    path2D_base: np.ndarray,
-    args: Namespace,
-    robot: SingleArmWholeBodyInterface,
-    t: int,
-    past_data: dict[str, deque[np.ndarray]],
-) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
-    p = robot.q[:2]
-
-    # NOTE: this one is obtained as the future path from path planner
-    max_base_v = np.linalg.norm(robot._max_v[:2])
-    trajectory_base = path2D_to_trajectory2D(args, path2D_base, max_base_v)
-    trajectory_base = np.hstack((trajectory_base, np.zeros((len(trajectory_base), 1))))
-
-    trajectorySE3_handlebar = construct_EE_path(args, p, past_data["path2D"])
-
-    if args.visualizer:
-        if t % int(np.ceil(args.ctrl_freq / 25)) == 0:
-            robot.visualizer_manager.sendCommand({"path": trajectory_base})
-            robot.visualizer_manager.sendCommand({"frame_path": trajectorySE3_handlebar})
-
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp.warmstartAndReSolve(x0, data=(trajectory_base, trajectorySE3_handlebar))
-    xs = np.array(ocp.solver.xs)
-    v_cmd = xs[1, robot.model.nq :]
-
-    err_vector_ee = log6(robot.T_w_e.actInv(trajectorySE3_handlebar[0]))
-    err_vector_base = np.linalg.norm(p - trajectory_base[0][:2])  # z axis is irrelevant
-    log_item = {}
-    log_item["err_vec_ee"] = err_vector_ee
-    log_item["err_norm_ee"] = np.linalg.norm(err_vector_ee).reshape((1,))
-    log_item["err_norm_base"] = np.linalg.norm(err_vector_base).reshape((1,))
-    save_past_item = {"path2D": p}
-    return v_cmd, save_past_item, log_item
-
-
-def initializePastData(
-    args: Namespace, T_w_e: SE3, p_base: np.ndarray, max_base_v: float
-) -> np.ndarray:
-    # prepopulate past data to make base and cart be on the same path in the past
-    # (which didn't actually happen because this just started)
-    p_ee = T_w_e.translation[:2]
-    straight_line_path = np.linspace(p_ee, p_base, args.past_window_size)
-    # straight_line_path_timed = path2D_timed(args, straight_line_path, max_base_v)
-    # return straight_line_path_timed # this one is shortened to args.n_knots! and we want the whole buffer
-    return straight_line_path
-
-
-def BaseAndEEPathFollowingMPC(
-    args: Namespace,
-    robot: SingleArmWholeBodyInterface,
-    path_planner: ProcessManager | types.FunctionType,
-run=True) -> None|ControlLoopManager:
-    """
-    BaseAndEEPathFollowingMPC
-    -----
-    run mpc for a point-to-point inverse kinematics.
-    note that the actual problem is solved on
-    a dynamics level, and velocities we command
-    are actually extracted from the state x(q,dq).
-    """
-
-    robot._mode = SingleArmWholeBodyInterface.control_mode.whole_body
-    T_w_e = robot.T_w_e
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp = BaseAndEEPathFollowingOCP(args, robot, x0)
-    ocp.solveInitialOCP(x0)
-
-    max_base_v = np.linalg.norm(robot._max_v[:2])
-
-    path2D_handlebar = initializePastData(args, T_w_e, robot.q[:2], float(max_base_v))
-
-    if type(path_planner) == types.FunctionType:
-        controlLoop = partial(
-# NOTE: DEPRICATED WILL GET DELETED SOON (TEMPLATE HANDLES IT NOW)
-            BaseAndEEPathFollowingMPCControlLoop, args, robot, ocp, path_planner
-        )
-    else:
-        get_position = lambda robot: robot.q[:2]
-        controlLoop = partial(
-            PathFollowingFromPlannerCtrllLoopTemplate,
-            path_planner,
-            get_position,
-            ocp,
-            BaseAndEEPathFollowingFromPlannerMPCControlLoop,
-            args,
-            robot,
-        )
-    log_item = {
-        "qs": np.zeros(robot.model.nq),
-        "dqs": np.zeros(robot.model.nv),
-        "err_vec_ee": np.zeros((6,)),
-        "err_norm_ee": np.zeros((1,)),
-        "err_norm_base": np.zeros((1,)),
-    }
-    save_past_dict = {"path2D": T_w_e.translation[:2]}
-    loop_manager = ControlLoopManager(
-        robot, controlLoop, args, save_past_dict, log_item
-    )
-
-    # actually put past data into the past window
-    loop_manager.past_data["path2D"].clear()
-    loop_manager.past_data["path2D"].extend(
-        path2D_handlebar[i] for i in range(args.past_window_size)
-    )
-
-    if run:
-        loop_manager.run()
-    else:
-        return loop_manager
-
-
-# TODO: the robot put in is a whole body robot,
-# which you need to implement
-def BaseAndDualEEPathFollowingMPCControlLoop(
-    T_absgoal_l: SE3,
-    T_absgoal_r: SE3,
-    ocp: CrocoOCP,
-    path2D_base: np.ndarray,
-    args: Namespace,
-    robot: DualArmWholeBodyInterface,
-    t: int,
-    past_data: dict[str, deque[np.ndarray]],
-) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
-    """
-    BaseAndDualEEPathFollowingMPCControlLoop
-    -----------------------------
-    cart pulling dual arm control loop
-    """
-    p = robot.q[:2]
-
-    # NOTE: this one is obtained as the future path from path planner
-    max_base_v = np.linalg.norm(robot._max_v[:2])
-    trajectory_base = path2D_to_trajectory2D(args, path2D_base, max_base_v)
-    trajectory_base = np.hstack((trajectory_base, np.zeros((len(trajectory_base), 1))))
-
-    trajectorySE3_handlebar = construct_EE_path(args, p, past_data["path2D"])
-    trajectorySE3_l = []
-    trajectorySE3_r = []
-    for traj_pose in trajectorySE3_handlebar:
-        trajectorySE3_l.append(T_absgoal_l.act(traj_pose))
-        trajectorySE3_r.append(T_absgoal_r.act(traj_pose))
-
-    if args.visualizer:
-        if t % int(np.ceil(args.ctrl_freq / 25)) == 0:
-            robot.visualizer_manager.sendCommand({"path": trajectory_base})
-            robot.visualizer_manager.sendCommand({"frame_path": trajectorySE3_handlebar})
-
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp.warmstartAndReSolve(x0, data=(trajectory_base, trajectorySE3_l, trajectorySE3_r))
-    xs = np.array(ocp.solver.xs)
-    v_cmd = xs[1, robot.model.nq :]
-
-    err_vector_ee_l = log6(robot.T_w_l.actInv(trajectorySE3_l[0]))
-    err_norm_ee_l = np.linalg.norm(err_vector_ee_l)
-    err_vector_ee_r = log6(robot.T_w_l.actInv(trajectorySE3_r[0]))
-    err_norm_ee_r = np.linalg.norm(err_vector_ee_r)
-    err_vector_base = np.linalg.norm(p - trajectory_base[0][:2])  # z axis is irrelevant
-    log_item = {}
-    log_item["err_vec_ee_l"] = err_vector_ee_l
-    log_item["err_norm_ee_l"] = err_norm_ee_l.reshape((1,))
-    log_item["err_vec_ee_r"] = err_vector_ee_r
-    log_item["err_norm_ee_r"] = err_norm_ee_r.reshape((1,))
-    log_item["err_norm_base"] = np.linalg.norm(err_vector_base).reshape((1,))
-    save_past_item = {"path2D": p}
-    return v_cmd, save_past_item, log_item
-
-def BaseAndDualEEPathFollowingMPC(
-    args: Namespace,
-    robot: DualArmWholeBodyInterface,
-    path_planner: ProcessManager | types.FunctionType,
-    T_absgoal_l: SE3,
-    T_absgoal_r: SE3,
-    run=True,
-) -> None | ControlLoopManager:
-    """
-    CrocoEndEffectorPathFollowingMPC
-    -----
-    run mpc for a point-to-point inverse kinematics.
-    note that the actual problem is solved on
-    a dynamics level, and velocities we command
-    are actually extracted from the state x(q,dq).
-    """
-    robot._mode = DualArmWholeBodyInterface.control_mode.whole_body
-    # NOTE: i'm shoving these into the class here - bad style,
-    # but i don't 
-    T_w_abs = robot.T_w_abs
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp = BaseAndDualArmEEPathFollowingOCP(args, robot, x0)
-    ocp.solveInitialOCP(x0)
-
-    max_base_v = np.linalg.norm(robot._max_v[:2])
-
-    path2D_handlebar = initializePastData(args, T_w_abs, robot.q[:2], float(max_base_v))
-
-    if type(path_planner) == types.FunctionType:
-        raise NotImplementedError
-    else:
-        get_position = lambda robot: robot.q[:2]
-        BaseAndDualEEPathFollowingMPCControlLoop_with_l_r = partial(BaseAndDualEEPathFollowingMPCControlLoop, T_absgoal_l, T_absgoal_r)
-        controlLoop = partial(
-            PathFollowingFromPlannerCtrllLoopTemplate,
-            path_planner,
-            get_position,
-            ocp,
-            BaseAndDualEEPathFollowingMPCControlLoop_with_l_r,
-            args,
-            robot,
-        )
-
-    log_item = {
-        "qs": np.zeros(robot.model.nq),
-        "dqs": np.zeros(robot.model.nv),
-        "err_vec_ee_l": np.zeros((6,)),
-        "err_norm_ee_l": np.zeros((1,)),
-        "err_vec_ee_r": np.zeros((6,)),
-        "err_norm_ee_r": np.zeros((1,)),
-        "err_norm_base": np.zeros((1,)),
-    }
-    save_past_dict = {"path2D": T_w_abs.translation[:2]}
-    loop_manager = ControlLoopManager(
-        robot, controlLoop, args, save_past_dict, log_item
-    )
-    # actually put past data into the past window
-    loop_manager.past_data["path2D"].clear()
-    loop_manager.past_data["path2D"].extend(
-        path2D_handlebar[i] for i in range(args.past_window_size)
-    )
-
-    if run:
-        loop_manager.run()
-    else:
-        return loop_manager
diff --git a/python/smc/control/optimal_control/croco_mpc_point_to_point.py b/python/smc/control/optimal_control/croco_mpc_point_to_point.py
deleted file mode 100644
index 379a8c4b1e4370d56bdb2f22dc528da211a3b92b..0000000000000000000000000000000000000000
--- a/python/smc/control/optimal_control/croco_mpc_point_to_point.py
+++ /dev/null
@@ -1,355 +0,0 @@
-from smc.control.controller_templates.point_to_point import (
-    BaseP2PCtrlLoopTemplate,
-    DualEEAndBaseP2PCtrlLoopTemplate,
-    DualEEP2PCtrlLoopTemplate,
-    EEAndBaseP2PCtrlLoopTemplate,
-    EEP2PCtrlLoopTemplate,
-)
-from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface
-from smc.robots.interfaces.single_arm_interface import SingleArmInterface
-from smc.robots.interfaces.dual_arm_interface import DualArmInterface
-from smc.robots.interfaces.whole_body_interface import (
-    SingleArmWholeBodyInterface,
-    DualArmWholeBodyInterface,
-)
-from smc.control.optimal_control.abstract_croco_ocp import CrocoOCP
-from smc.control.optimal_control.point_to_point_croco_ocp import (
-    BaseIKOCP,
-    SingleArmIKOCP,
-    DualArmIKOCP,
-    BaseAndSingleArmIKOCP,
-    BaseAndDualArmIKOCP,
-)
-from smc.control.control_loop_manager import ControlLoopManager
-
-import pinocchio as pin
-import numpy as np
-from functools import partial
-from collections import deque
-from argparse import Namespace
-
-
-def CrocoBaseP2PMPCControlLoop(
-    ocp: CrocoOCP,
-    p_basegoal: np.ndarray,
-    args: Namespace,
-    robot: MobileBaseInterface,
-    _: int,
-    __: dict[str, deque[np.ndarray]],
-) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
-    """
-    CrocoIKMPCControlLoop
-    ---------------------
-    """
-    # set initial state from sensor
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp.warmstartAndReSolve(x0)
-    xs = np.array(ocp.solver.xs)
-    # NOTE: for some reason the first value is always some wild bs
-    vel_cmd = xs[1, robot.model.nq :]
-    return vel_cmd, {}, {}
-
-
-def CrocoBaseP2PMPC(
-    args: Namespace, robot: MobileBaseInterface, p_basegoal: np.ndarray, run=True
-):
-    """
-    CrocoBaseP2PMPC
-    -----
-    base point-to-point task
-    """
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp = BaseIKOCP(args, robot, x0, p_basegoal)
-    ocp.solveInitialOCP(x0)
-
-    controlLoop = partial(
-        BaseP2PCtrlLoopTemplate,
-        ocp,
-        p_basegoal,
-        CrocoBaseP2PMPCControlLoop,
-        args,
-        robot,
-    )
-    log_item = {
-        "qs": np.zeros(robot.nq),
-        "base_err_norm": np.zeros(1),
-        "dqs": np.zeros(robot.nv),
-        "dqs_cmd": np.zeros(robot.nv),
-    }
-    save_past_dict = {}
-    loop_manager = ControlLoopManager(
-        robot, controlLoop, args, save_past_dict, log_item
-    )
-    if run:
-        loop_manager.run()
-    else:
-        return loop_manager
-
-
-def CrocoEEP2PMPCControlLoop(
-    ocp: CrocoOCP,
-    T_w_goal: pin.SE3,
-    args: Namespace,
-    robot: SingleArmInterface,
-    _: int,
-    __: dict[str, deque[np.ndarray]],
-) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
-    """
-    CrocoIKMPCControlLoop
-    ---------------------
-    """
-    # set initial state from sensor
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp.warmstartAndReSolve(x0)
-    xs = np.array(ocp.solver.xs)
-    # NOTE: for some reason the first value is always some wild bs
-    vel_cmd = xs[1, robot.model.nq :]
-    return vel_cmd, {}, {}
-
-
-def CrocoEEP2PMPC(
-    args: Namespace, robot: SingleArmInterface, T_w_goal: pin.SE3, run=True
-):
-    """
-    IKMPC
-    -----
-    run mpc for a point-to-point inverse kinematics.
-    note that the actual problem is solved on
-    a dynamics level, and velocities we command
-    are actually extracted from the state x(q,dq)
-    """
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp = SingleArmIKOCP(args, robot, x0, T_w_goal)
-    ocp.solveInitialOCP(x0)
-
-    controlLoop = partial(
-        EEP2PCtrlLoopTemplate, ocp, T_w_goal, CrocoEEP2PMPCControlLoop, args, robot
-    )
-    log_item = {
-        "qs": np.zeros(robot.nq),
-        "err_norm": np.zeros(1),
-        "dqs": np.zeros(robot.nv),
-        "dqs_cmd": np.zeros(robot.nv),
-    }
-    save_past_dict = {}
-    loop_manager = ControlLoopManager(
-        robot, controlLoop, args, save_past_dict, log_item
-    )
-    if run:
-        loop_manager.run()
-    else:
-        return loop_manager
-
-
-def CrocoDualEEP2PMPCControlLoop(
-    ocp: CrocoOCP,
-    T_w_abseegoal: pin.SE3,
-    T_absgoal_lgoal: pin.SE3,
-    T_absgoal_rgoal: pin.SE3,
-    args: Namespace,
-    robot: DualArmInterface,
-    t: int,
-    past_data: dict[str, deque[np.ndarray]],
-) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
-    """
-    CrocoIKMPCControlLoop
-    ---------------------
-    """
-    # set initial state from sensor
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp.warmstartAndReSolve(x0)
-    xs = np.array(ocp.solver.xs)
-    # NOTE: for some reason the first value is always some wild bs
-    vel_cmd = xs[1, robot.model.nq :]
-    return vel_cmd, {}, {}
-
-
-def CrocoDualEEP2PMPC(
-    args: Namespace,
-    robot: DualArmInterface,
-    T_w_absgoal: pin.SE3,
-    T_absgoal_l: pin.SE3,
-    T_absgoal_r: pin.SE3,
-    run=True,
-):
-    """
-    DualEEP2PMPC
-    -----
-    run mpc for a point-to-point inverse kinematics.
-    note that the actual problem is solved on
-    a dynamics level, and velocities we command
-    are actually extracted from the state x(q,dq)
-    """
-    x0 = np.concatenate([robot.q, robot.v])
-    T_w_lgoal = T_absgoal_l.act(T_w_absgoal)
-    T_w_rgoal = T_absgoal_r.act(T_w_absgoal)
-
-    ocp = DualArmIKOCP(args, robot, x0, (T_w_lgoal, T_w_rgoal))
-    ocp.solveInitialOCP(x0)
-
-    controlLoop = partial(
-        DualEEP2PCtrlLoopTemplate,
-        ocp,
-        T_w_absgoal,
-        T_absgoal_l,
-        T_absgoal_r,
-        CrocoDualEEP2PMPCControlLoop,
-        args,
-        robot,
-    )
-    log_item = {
-        "qs": np.zeros(robot.nq),
-        "dqs": np.zeros(robot.nv),
-        "dqs_cmd": np.zeros(robot.nv),
-        "l_err_norm": np.zeros(1),
-        "r_err_norm": np.zeros(1),
-    }
-    save_past_dict = {}
-    loop_manager = ControlLoopManager(
-        robot, controlLoop, args, save_past_dict, log_item
-    )
-    if run:
-        loop_manager.run()
-    else:
-        return loop_manager
-
-
-def CrocoP2PEEAndBaseMPCControlLoop(
-    ocp: CrocoOCP,
-    T_w_eegoal: pin.SE3,
-    p_basegoal: np.ndarray,
-    args: Namespace,
-    robot: SingleArmWholeBodyInterface,
-    t: int,
-    past_data: dict[str, deque[np.ndarray]],
-) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
-    """
-    CrocoP2PEEAndBaseMPCControlLoop
-    ---------------------
-    """
-    # set initial state from sensor
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp.warmstartAndReSolve(x0)
-    xs = np.array(ocp.solver.xs)
-    # NOTE: for some reason the first value is always some wild bs
-    vel_cmd = xs[1, robot.model.nq :]
-    return vel_cmd, {}, {}
-
-
-def CrocoEEAndBaseP2PMPC(
-    args, robot, T_w_eegoal: pin.SE3, p_basegoal: np.ndarray, run=True
-):
-    """
-    IKMPC
-    -----
-    run mpc for a point-to-point inverse kinematics.
-    note that the actual problem is solved on
-    a dynamics level, and velocities we command
-    are actually extracted from the state x(q,dq)
-    """
-    x0 = np.concatenate([robot.q, robot.v])
-    goal = (T_w_eegoal, p_basegoal)
-    ocp = BaseAndSingleArmIKOCP(args, robot, x0, goal)
-    ocp.solveInitialOCP(x0)
-
-    controlLoop = partial(
-        EEAndBaseP2PCtrlLoopTemplate,
-        ocp,
-        T_w_eegoal,
-        p_basegoal,
-        CrocoP2PEEAndBaseMPCControlLoop,
-        args,
-        robot,
-    )
-    log_item = {
-        "qs": np.zeros(robot.nq),
-        "dqs": np.zeros(robot.nv),
-        "dqs_cmd": np.zeros(robot.nv),
-        "ee_err_norm": np.zeros(1),
-        "base_err_norm": np.zeros(1),
-    }
-    save_past_dict = {}
-    loop_manager = ControlLoopManager(
-        robot, controlLoop, args, save_past_dict, log_item
-    )
-    if run:
-        loop_manager.run()
-    else:
-        return loop_manager
-
-
-def CrocoP2PDualEEAndBaseMPCControlLoop(
-    ocp,
-    T_w_absgoal: pin.SE3,
-    T_absgoal_l: pin.SE3,
-    T_absgoal_r: pin.SE3,
-    p_basegoal: np.ndarray,
-    args: Namespace,
-    robot: DualArmWholeBodyInterface,
-    t: int,
-    past_data: dict[str, deque[np.ndarray]],
-) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
-    """
-    CrocoP2PEEAndBaseMPCControlLoop
-    ---------------------
-    """
-    x0 = np.concatenate([robot.q, robot.v])
-    ocp.warmstartAndReSolve(x0)
-    xs = np.array(ocp.solver.xs)
-    # NOTE: for some reason the first value is always some wild bs
-    v_cmd = xs[1, robot.model.nq :]
-
-    return v_cmd, {}, {}
-
-
-def CrocoDualEEAndBaseP2PMPC(
-    args: Namespace,
-    robot: DualArmWholeBodyInterface,
-    T_w_absgoal: pin.SE3,
-    T_absgoal_l: pin.SE3,
-    T_absgoal_r: pin.SE3,
-    p_basegoal: np.ndarray,
-    run=True,
-) -> None | ControlLoopManager:
-    """
-    IKMPC
-    -----
-    run mpc for a point-to-point inverse kinematics.
-    note that the actual problem is solved on
-    a dynamics level, and velocities we command
-    are actually extracted from the state x(q,dq)
-    """
-    x0 = np.concatenate([robot.q, robot.v])
-    T_w_lgoal = T_absgoal_l.act(T_w_absgoal)
-    T_w_rgoal = T_absgoal_r.act(T_w_absgoal)
-    goal = (T_w_lgoal, T_w_rgoal, p_basegoal)
-    ocp = BaseAndDualArmIKOCP(args, robot, x0, goal)
-    ocp.solveInitialOCP(x0)
-
-    controlLoop = partial(
-        DualEEAndBaseP2PCtrlLoopTemplate,
-        ocp,
-        T_w_absgoal,
-        T_absgoal_l,
-        T_absgoal_r,
-        p_basegoal,
-        CrocoP2PDualEEAndBaseMPCControlLoop,
-        args,
-        robot,
-    )
-    log_item = {
-        "qs": np.zeros(robot.nq),
-        "dqs": np.zeros(robot.nv),
-        "dqs_cmd": np.zeros(robot.nv),
-        "l_err_norm": np.zeros(1),
-        "r_err_norm": np.zeros(1),
-        "base_err_norm": np.zeros(1),
-    }
-    save_past_dict = {}
-    loop_manager = ControlLoopManager(
-        robot, controlLoop, args, save_past_dict, log_item
-    )
-    if run:
-        loop_manager.run()
-    else:
-        return loop_manager
diff --git a/python/smc/control/optimal_control/croco_path_following/mpc/base_and_dual_arm_reference_mpc.py b/python/smc/control/optimal_control/croco_path_following/mpc/base_and_dual_arm_reference_mpc.py
new file mode 100644
index 0000000000000000000000000000000000000000..4912177eedd7d80f1a09efa7150d8e6a6189f40b
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_path_following/mpc/base_and_dual_arm_reference_mpc.py
@@ -0,0 +1,171 @@
+from smc.robots.interfaces.whole_body_interface import (
+    DualArmWholeBodyInterface,
+)
+from smc.control.control_loop_manager import ControlLoopManager
+from smc.multiprocessing.process_manager import ProcessManager
+from smc.control.optimal_control.abstract_croco_ocp import CrocoOCP
+from smc.control.optimal_control.croco_path_following.ocp.base_and_dual_arm_reference_ocp import (
+    BaseAndDualArmEEPathFollowingOCP,
+)
+from smc.path_generation.path_math.path2d_to_6d import (
+    path2D_to_SE3,
+)
+from smc.path_generation.path_math.cart_pulling_path_math import (
+    construct_EE_path,
+)
+from smc.path_generation.path_math.path_to_trajectory import (
+    path2D_to_trajectory2D,
+    pathSE3_to_trajectorySE3,
+)
+from smc.control.controller_templates.path_following_template import (
+    PathFollowingFromPlannerCtrllLoopTemplate,
+)
+
+import numpy as np
+from functools import partial
+import types
+from argparse import Namespace
+from pinocchio import SE3, log6
+from collections import deque
+
+
+def initializePastData(
+    args: Namespace, T_w_e: SE3, p_base: np.ndarray, max_base_v: float
+) -> np.ndarray:
+    # prepopulate past data to make base and cart be on the same path in the past
+    # (which didn't actually happen because this just started)
+    p_ee = T_w_e.translation[:2]
+    straight_line_path = np.linspace(p_ee, p_base, args.past_window_size)
+    # straight_line_path_timed = path2D_timed(args, straight_line_path, max_base_v)
+    # return straight_line_path_timed # this one is shortened to args.n_knots! and we want the whole buffer
+    return straight_line_path
+
+
+# TODO: the robot put in is a whole body robot,
+# which you need to implement
+def BaseAndDualEEPathFollowingMPCControlLoop(
+    T_absgoal_l: SE3,
+    T_absgoal_r: SE3,
+    ocp: CrocoOCP,
+    path2D_base: np.ndarray,
+    args: Namespace,
+    robot: DualArmWholeBodyInterface,
+    t: int,
+    past_data: dict[str, deque[np.ndarray]],
+) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
+    """
+    BaseAndDualEEPathFollowingMPCControlLoop
+    -----------------------------
+    cart pulling dual arm control loop
+    """
+    p = robot.q[:2]
+
+    # NOTE: this one is obtained as the future path from path planner
+    max_base_v = np.linalg.norm(robot._max_v[:2])
+    trajectory_base = path2D_to_trajectory2D(args, path2D_base, max_base_v)
+    trajectory_base = np.hstack((trajectory_base, np.zeros((len(trajectory_base), 1))))
+
+    trajectorySE3_handlebar = construct_EE_path(args, p, past_data["path2D"])
+    trajectorySE3_l = []
+    trajectorySE3_r = []
+    for traj_pose in trajectorySE3_handlebar:
+        trajectorySE3_l.append(T_absgoal_l.act(traj_pose))
+        trajectorySE3_r.append(T_absgoal_r.act(traj_pose))
+
+    if args.visualizer:
+        if t % int(np.ceil(args.ctrl_freq / 25)) == 0:
+            robot.visualizer_manager.sendCommand({"path": trajectory_base})
+            robot.visualizer_manager.sendCommand(
+                {"frame_path": trajectorySE3_handlebar}
+            )
+
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp.warmstartAndReSolve(
+        x0, data=(trajectory_base, trajectorySE3_l, trajectorySE3_r)
+    )
+    xs = np.array(ocp.solver.xs)
+    v_cmd = xs[1, robot.model.nq :]
+
+    err_vector_ee_l = log6(robot.T_w_l.actInv(trajectorySE3_l[0]))
+    err_norm_ee_l = np.linalg.norm(err_vector_ee_l)
+    err_vector_ee_r = log6(robot.T_w_r.actInv(trajectorySE3_r[0]))
+    err_norm_ee_r = np.linalg.norm(err_vector_ee_r)
+    err_vector_base = np.linalg.norm(p - trajectory_base[0][:2])  # z axis is irrelevant
+    log_item = {}
+    log_item["err_vec_ee_l"] = err_vector_ee_l
+    log_item["err_norm_ee_l"] = err_norm_ee_l.reshape((1,))
+    log_item["err_vec_ee_r"] = err_vector_ee_r
+    log_item["err_norm_ee_r"] = err_norm_ee_r.reshape((1,))
+    log_item["err_norm_base"] = np.linalg.norm(err_vector_base).reshape((1,))
+    save_past_item = {"path2D": p}
+    return v_cmd, save_past_item, log_item
+
+
+def BaseAndDualEEPathFollowingMPC(
+    args: Namespace,
+    robot: DualArmWholeBodyInterface,
+    path_planner: ProcessManager | types.FunctionType,
+    T_absgoal_l: SE3,
+    T_absgoal_r: SE3,
+    run=True,
+) -> None | ControlLoopManager:
+    """
+    CrocoEndEffectorPathFollowingMPC
+    -----
+    run mpc for a point-to-point inverse kinematics.
+    note that the actual problem is solved on
+    a dynamics level, and velocities we command
+    are actually extracted from the state x(q,dq).
+    """
+    robot._mode = DualArmWholeBodyInterface.control_mode.whole_body
+    # NOTE: i'm shoving these into the class here - bad style,
+    # but i don't
+    T_w_abs = robot.T_w_abs
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp = BaseAndDualArmEEPathFollowingOCP(args, robot, x0)
+    ocp.solveInitialOCP(x0)
+
+    max_base_v = np.linalg.norm(robot._max_v[:2])
+
+    path2D_handlebar = initializePastData(args, T_w_abs, robot.q[:2], float(max_base_v))
+
+    if type(path_planner) == types.FunctionType:
+        raise NotImplementedError
+    else:
+        get_position = lambda robot: robot.q[:2]
+        BaseAndDualEEPathFollowingMPCControlLoop_with_l_r = partial(
+            BaseAndDualEEPathFollowingMPCControlLoop, T_absgoal_l, T_absgoal_r
+        )
+        controlLoop = partial(
+            PathFollowingFromPlannerCtrllLoopTemplate,
+            path_planner,
+            get_position,
+            ocp,
+            BaseAndDualEEPathFollowingMPCControlLoop_with_l_r,
+            args,
+            robot,
+        )
+
+    log_item = {
+        "qs": np.zeros(robot.model.nq),
+        "dqs": np.zeros(robot.model.nv),
+        "err_vec_ee_l": np.zeros((6,)),
+        "err_norm_ee_l": np.zeros((1,)),
+        "err_vec_ee_r": np.zeros((6,)),
+        "err_norm_ee_r": np.zeros((1,)),
+        "err_norm_base": np.zeros((1,)),
+    }
+    save_past_dict = {"path2D": T_w_abs.translation[:2]}
+    loop_manager = ControlLoopManager(
+        robot, controlLoop, args, save_past_dict, log_item
+    )
+    # actually put past data into the past window
+    loop_manager.past_data["path2D"].clear()
+    loop_manager.past_data["path2D"].extend(
+        path2D_handlebar[i] for i in range(args.past_window_size)
+    )
+
+    if run:
+        loop_manager.run()
+    else:
+        return loop_manager
diff --git a/python/smc/control/optimal_control/croco_path_following/mpc/base_and_single_arm_reference_mpc.py b/python/smc/control/optimal_control/croco_path_following/mpc/base_and_single_arm_reference_mpc.py
new file mode 100644
index 0000000000000000000000000000000000000000..4ba2af49b880050e09f538c9e8e1475f84c1c42b
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_path_following/mpc/base_and_single_arm_reference_mpc.py
@@ -0,0 +1,115 @@
+from smc.robots.interfaces.whole_body_interface import (
+    SingleArmWholeBodyInterface,
+)
+from smc.control.control_loop_manager import ControlLoopManager
+from smc.multiprocessing.process_manager import ProcessManager
+from smc.control.optimal_control.abstract_croco_ocp import CrocoOCP
+from smc.control.optimal_control.croco_path_following.ocp.base_and_single_arm_reference_ocp import (
+    BaseAndEEPathFollowingOCP,
+)
+from smc.path_generation.path_math.path2d_to_6d import (
+    path2D_to_SE3,
+)
+from smc.path_generation.path_math.cart_pulling_path_math import (
+    construct_EE_path,
+)
+from smc.path_generation.path_math.path_to_trajectory import (
+    path2D_to_trajectory2D,
+    pathSE3_to_trajectorySE3,
+)
+from smc.control.controller_templates.path_following_template import (
+    PathFollowingFromPlannerCtrllLoopTemplate,
+)
+
+import numpy as np
+from functools import partial
+import types
+from argparse import Namespace
+from pinocchio import SE3, log6
+from collections import deque
+
+
+def BaseAndEEPathFollowingMPCControlLoop(
+    ocp: CrocoOCP,
+    path: tuple[np.ndarray, list[SE3]],
+    args: Namespace,
+    robot,
+    t: int,
+    _: dict[str, deque[np.ndarray]],
+) -> tuple[np.ndarray, dict[str, np.ndarray]]:
+    """
+    BaseAndEEPathFollowingMPCControlLoop
+    -----------------------------
+    both a path for both the base and the end-effector are provided,
+    and both are followed
+    """
+    breakFlag = False
+    log_item = {}
+    save_past_dict = {}
+
+    q = robot.q
+    T_w_e = robot.T_w_e
+    path_base, path_EE = path_planner(T_w_e)
+
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp.warmstartAndReSolve(x0, data=(path_base, path_EE))
+    xs = np.array(ocp.solver.xs)
+    us = np.array(ocp.solver.us)
+    vel_cmds = xs[1, robot.model.nq :]
+
+    robot.sendVelocityCommand(vel_cmds)
+    if t % int(np.ceil(args.ctrl_freq / 25)) == 0:
+        path_base = np.array(path_base)
+        robot.visualizer_manager.sendCommand({"path": path_base})
+        robot.visualizer_manager.sendCommand({"frame_path": path_EE})
+
+    err_vector_ee = log6(T_w_e.actInv(path_EE[0]))
+    err_vector_base = np.linalg.norm(q[:2] - path_base[0][:2])  # z axis is irrelevant
+    log_item["err_vec_ee"] = err_vector_ee
+    log_item["err_norm_ee"] = np.linalg.norm(err_vector_ee).reshape((1,))
+    log_item["err_norm_base"] = np.linalg.norm(err_vector_base).reshape((1,))
+    log_item["qs"] = q.reshape((robot.model.nq,))
+    log_item["dqs"] = robot.v.reshape((robot.model.nv,))
+    return breakFlag, save_past_dict, log_item
+
+
+def BaseAndEEPathFollowingMPC(
+    args: Namespace,
+    robot: SingleArmWholeBodyInterface,
+    path_planner: ProcessManager | types.FunctionType,
+    run=True,
+) -> None | ControlLoopManager:
+    """
+    BaseAndEEPathFollowingMPC
+    -----
+    run mpc for a point-to-point inverse kinematics.
+    note that the actual problem is solved on
+    a dynamics level, and velocities we command
+    are actually extracted from the state x(q,dq).
+    """
+
+    robot._mode = SingleArmWholeBodyInterface.control_mode.whole_body
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp = BaseAndEEPathFollowingOCP(args, robot, x0)
+    ocp.solveInitialOCP(x0)
+
+    controlLoop = partial(
+        BaseAndEEPathFollowingMPCControlLoop,
+        args,
+        robot,
+        ocp,
+        path_planner,
+    )
+    log_item = {
+        "qs": np.zeros(robot.model.nq),
+        "dqs": np.zeros(robot.model.nv),
+        "err_vec_ee": np.zeros((6,)),
+        "err_norm_ee": np.zeros((1,)),
+        "err_norm_base": np.zeros((1,)),
+    }
+    loop_manager = ControlLoopManager(robot, controlLoop, args, {}, log_item)
+
+    if run:
+        loop_manager.run()
+    else:
+        return loop_manager
diff --git a/python/smc/control/optimal_control/croco_path_following/mpc/base_reference_mpc.py b/python/smc/control/optimal_control/croco_path_following/mpc/base_reference_mpc.py
new file mode 100644
index 0000000000000000000000000000000000000000..8a1399373896312db1bc69141a2e34ae7880fb7a
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_path_following/mpc/base_reference_mpc.py
@@ -0,0 +1,98 @@
+from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface
+from smc.control.control_loop_manager import ControlLoopManager
+from smc.multiprocessing.process_manager import ProcessManager
+from smc.control.optimal_control.croco_path_following.ocp.base_reference_ocp import (
+    BasePathFollowingOCP,
+)
+from smc.control.controller_templates.path_following_template import (
+    PathFollowingFromPlannerCtrllLoopTemplate,
+)
+from smc.path_generation.path_math.path_to_trajectory import path2D_to_trajectory2D
+
+import numpy as np
+from functools import partial
+import types
+from argparse import Namespace
+from collections import deque
+
+
+def CrocoBasePathFollowingMPCControlLoop(
+    ocp: BasePathFollowingOCP,
+    path2D: np.ndarray,
+    args: Namespace,
+    robot: MobileBaseInterface,
+    t: int,
+    _: dict[str, deque[np.ndarray]],
+) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
+
+    p = robot.T_w_b.translation[:2]
+    max_base_v = np.linalg.norm(robot._max_v[:2])
+    path_base = path2D_to_trajectory2D(args, path2D, max_base_v)
+    path_base = np.hstack((path_base, np.zeros((len(path_base), 1))))
+
+    if args.visualizer:
+        if t % int(np.ceil(args.ctrl_freq / 25)) == 0:
+            robot.visualizer_manager.sendCommand({"path": path_base})
+
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp.warmstartAndReSolve(x0, data=(path_base))
+    xs = np.array(ocp.solver.xs)
+    v_cmd = xs[1, robot.model.nq :]
+    # NOTE: we might get stuck
+    # TODO: make it more robust, it can still get stuck with this
+    # a good idea is to do this for a some number of iterations -
+    # you can keep this them in past data
+    if np.linalg.norm(v_cmd) < 0.05:
+        print(t, "RESOLVING FOR ONLY FINAL PATH POINT")
+        last_point_only = np.ones((len(path_base), 2))
+        last_point_only = np.hstack((last_point_only, np.zeros((len(path_base), 1))))
+        last_point_only = last_point_only * path_base[-1]
+        ocp.warmstartAndReSolve(x0, data=(last_point_only))
+        xs = np.array(ocp.solver.xs)
+        v_cmd = xs[1, robot.model.nq :]
+
+    err_vector_base = np.linalg.norm(p - path_base[0][:2])  # z axis is irrelevant
+    log_item = {}
+    log_item["err_norm_base"] = np.linalg.norm(err_vector_base).reshape((1,))
+    return v_cmd, {}, log_item
+
+
+def CrocoBasePathFollowingMPC(
+    args: Namespace,
+    robot: MobileBaseInterface,
+    x0: np.ndarray,
+    path_planner: ProcessManager | types.FunctionType,
+    run=True,
+) -> None | ControlLoopManager:
+    """
+    CrocoBasePathFollowingMPC
+    -----
+    """
+
+    ocp = BasePathFollowingOCP(args, robot, x0)
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp.solveInitialOCP(x0)
+
+    get_position = lambda robot: robot.T_w_b.translation[:2]
+    controlLoop = partial(
+        PathFollowingFromPlannerCtrllLoopTemplate,
+        path_planner,
+        get_position,
+        ocp,
+        CrocoBasePathFollowingMPCControlLoop,
+        args,
+        robot,
+    )
+    log_item = {
+        "qs": np.zeros(robot.nq),
+        "dqs": np.zeros(robot.nv),
+        "err_norm_base": np.zeros((1,)),
+    }
+    save_past_item = {}
+    loop_manager = ControlLoopManager(
+        robot, controlLoop, args, save_past_item, log_item
+    )
+    if run:
+        loop_manager.run()
+    else:
+        return loop_manager
diff --git a/python/smc/control/optimal_control/croco_path_following/mpc/dual_arm_reference_mpc.py b/python/smc/control/optimal_control/croco_path_following/mpc/dual_arm_reference_mpc.py
new file mode 100644
index 0000000000000000000000000000000000000000..bdf94dba262227c21efdfa7e7387e60c33f1d539
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_path_following/mpc/dual_arm_reference_mpc.py
@@ -0,0 +1,135 @@
+from smc.robots.interfaces.dual_arm_interface import DualArmInterface
+from smc.control.optimal_control.croco_path_following.ocp.dual_arm_reference_ocp import (
+    DualArmEEPathFollowingOCP,
+)
+from smc.control.controller_templates.path_following_template import (
+    PathFollowingFromPlannerCtrllLoopTemplate,
+)
+from smc.path_generation.path_math.path2d_to_6d import (
+    path2D_to_SE3,
+)
+from smc.path_generation.path_math.path_to_trajectory import (
+    path2D_to_trajectory2D,
+    pathSE3_to_trajectorySE3,
+)
+from smc.control.control_loop_manager import ControlLoopManager
+from smc.multiprocessing.process_manager import ProcessManager
+
+import numpy as np
+from functools import partial
+import types
+from argparse import Namespace
+from pinocchio import SE3, log6
+from collections import deque
+
+
+def CrocoDualArmEEPathFollowingMPCControlLoop(
+    T_absgoal_l: SE3,
+    T_absgoal_r: SE3,
+    ocp: DualArmEEPathFollowingOCP,
+    path: np.ndarray | list[SE3],  # either 2D or pose
+    args: Namespace,
+    robot: DualArmInterface,
+    t: int,
+    _: dict[str, deque[np.ndarray]],
+) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
+    """
+    CrocoEEPathFollowingMPCControlLoop
+    -----------------------------
+    end-effectors follows the prescribed path.
+    the path is defined with T_w_abs, from which
+    T_w_l and T_w_r are defined via T_absgoal_l and T_absgoal_r.
+
+    path planner can either be a function which spits out a list of path points
+    or an instance of ProcessManager which spits out path points
+    by calling ProcessManager.getData()
+    """
+    max_base_v = np.linalg.norm(robot._max_v[:2])
+    perc_of_max_v = 0.5
+    velocity = perc_of_max_v * max_base_v
+    if type(path) == np.ndarray:
+        trajectory2D = path2D_to_trajectory2D(args, path, velocity)
+        trajectory_T_w_abs = path2D_to_SE3(trajectory2D, args.handlebar_height)
+    else:
+        trajectory_T_w_abs = pathSE3_to_trajectorySE3(args, path, velocity)
+
+    trajectory_T_w_l = []
+    trajectory_T_w_r = []
+    for traj_T_w_abs in trajectory_T_w_abs:
+        trajectory_T_w_l.append(T_absgoal_l.act(traj_T_w_abs))
+        trajectory_T_w_r.append(T_absgoal_r.act(traj_T_w_abs))
+
+    # TODO: EVIL AND HAS TO BE REMOVED FROM HERE
+    if args.visualizer:
+        if t % int(np.ceil(args.ctrl_freq / 25)) == 0:
+            robot.visualizer_manager.sendCommand({"frame_path": trajectory_T_w_abs})
+
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp.warmstartAndReSolve(x0, data=(trajectory_T_w_l, trajectory_T_w_r))
+    xs = np.array(ocp.solver.xs)
+    v_cmd = xs[1, robot.model.nq :]
+
+    err_vector_ee_l = log6(robot.T_w_l.actInv(trajectory_T_w_l[0])).vector
+    err_vector_ee_r = log6(robot.T_w_r.actInv(trajectory_T_w_r[0])).vector
+    log_item = {"err_vec_ee_l": err_vector_ee_l}
+    log_item = {"err_vec_ee_r": err_vector_ee_r}
+
+    return v_cmd, {}, log_item
+
+
+def CrocoDualArmEEPathFollowingMPC(
+    args: Namespace,
+    robot: DualArmInterface,
+    T_absgoal_l: SE3,
+    T_absgoal_r: SE3,
+    x0: np.ndarray,
+    path_planner: ProcessManager | types.FunctionType,
+    run=True,
+) -> None | ControlLoopManager:
+    """
+    CrocoEndEffectorPathFollowingMPC
+    -----
+    follow a fixed pre-determined path, or a path received from a planner.
+    the path does NOT need to start from your current pose - you need to get to it yourself.
+    """
+
+    ocp = DualArmEEPathFollowingOCP(args, robot, x0)
+    # technically should be done in controlloop because now
+    # it's solved 2 times before the first command,
+    # but we don't have time for details rn
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp.solveInitialOCP(x0)
+
+    # NOTE: a bit of dirty hacking for now because
+    # i know that the only planner i have gives 2D references.
+    # but of course otherwise the only reasonable
+    # thing is that the planner gives an SE3 reference
+    # to a MPC following an SE3 reference
+    if type(path_planner) == ProcessManager:
+        get_position = lambda robot: robot.T_w_abs.translation[:2]
+    else:
+        get_position = lambda robot: robot.T_w_abs
+    loop = partial(CrocoDualArmEEPathFollowingMPCControlLoop, T_absgoal_l, T_absgoal_r)
+    controlLoop = partial(
+        PathFollowingFromPlannerCtrllLoopTemplate,
+        path_planner,
+        get_position,
+        ocp,
+        loop,
+        args,
+        robot,
+    )
+    log_item = {
+        "qs": np.zeros(robot.model.nq),
+        "dqs": np.zeros(robot.model.nv),
+        "err_vec_ee_l": np.zeros(6),
+        "err_vec_ee_r": np.zeros(6),
+    }
+    save_past_item = {}
+    loop_manager = ControlLoopManager(
+        robot, controlLoop, args, save_past_item, log_item
+    )
+    if run:
+        loop_manager.run()
+    else:
+        return loop_manager
diff --git a/python/smc/control/optimal_control/croco_path_following/mpc/single_arm_reference_mpc.py b/python/smc/control/optimal_control/croco_path_following/mpc/single_arm_reference_mpc.py
new file mode 100644
index 0000000000000000000000000000000000000000..08aae3ec29efd2ac260fdd8e9be60e667c67203e
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_path_following/mpc/single_arm_reference_mpc.py
@@ -0,0 +1,119 @@
+from smc.robots.interfaces.single_arm_interface import SingleArmInterface
+from smc.control.optimal_control.croco_path_following.ocp.single_arm_reference_ocp import (
+    CrocoEEPathFollowingOCP,
+)
+from smc.control.controller_templates.path_following_template import (
+    PathFollowingFromPlannerCtrllLoopTemplate,
+)
+from smc.path_generation.path_math.path2d_to_6d import (
+    path2D_to_SE3,
+)
+from smc.path_generation.path_math.path_to_trajectory import (
+    path2D_to_trajectory2D,
+    pathSE3_to_trajectorySE3,
+)
+from smc.control.control_loop_manager import ControlLoopManager
+from smc.multiprocessing.process_manager import ProcessManager
+
+import numpy as np
+from functools import partial
+import types
+from argparse import Namespace
+from pinocchio import SE3, log6
+from collections import deque
+
+
+def CrocoEEPathFollowingMPCControlLoop(
+    ocp: CrocoEEPathFollowingOCP,
+    path: np.ndarray | list[SE3],  # either 2D or pose
+    args: Namespace,
+    robot: SingleArmInterface,
+    t: int,
+    _: dict[str, deque[np.ndarray]],
+) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
+    """
+    CrocoPathFollowingMPCControlLoop
+    -----------------------------
+    end-effector follows the prescribed path.
+
+    path planner can either be a function which spits out a list of path points
+    or an instance of ProcessManager which spits out path points
+    by calling ProcessManager.getData()
+    """
+    max_base_v = np.linalg.norm(robot._max_v[:2])
+    perc_of_max_v = 0.5
+    velocity = perc_of_max_v * max_base_v
+    if type(path) == np.ndarray:
+        trajectory2D = path2D_to_trajectory2D(args, path, velocity)
+        trajectory_EE_SE3 = path2D_to_SE3(trajectory2D, args.handlebar_height)
+    else:
+        trajectory_EE_SE3 = pathSE3_to_trajectorySE3(args, path, velocity)
+
+    # TODO: EVIL AND HAS TO BE REMOVED FROM HERE
+    if args.visualizer:
+        if t % int(np.ceil(args.ctrl_freq / 25)) == 0:
+            robot.visualizer_manager.sendCommand({"frame_path": trajectory_EE_SE3})
+
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp.warmstartAndReSolve(x0, data=trajectory_EE_SE3)
+    xs = np.array(ocp.solver.xs)
+    v_cmd = xs[1, robot.model.nq :]
+
+    err_vector_ee = log6(robot.T_w_e.actInv(trajectory_EE_SE3[0])).vector
+    log_item = {"err_vec_ee": err_vector_ee}
+
+    return v_cmd, {}, log_item
+
+
+def CrocoEEPathFollowingMPC(
+    args: Namespace,
+    robot: SingleArmInterface,
+    x0: np.ndarray,
+    path_planner: ProcessManager | types.FunctionType,
+    run=True,
+) -> None | ControlLoopManager:
+    """
+    CrocoEndEffectorPathFollowingMPC
+    -----
+    follow a fixed pre-determined path, or a path received from a planner.
+    the path does NOT need to start from your current pose - you need to get to it yourself.
+    """
+
+    ocp = CrocoEEPathFollowingOCP(args, robot, x0)
+    # technically should be done in controlloop because now
+    # it's solved 2 times before the first command,
+    # but we don't have time for details rn
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp.solveInitialOCP(x0)
+
+    # NOTE: a bit of dirty hacking for now because
+    # i know that the only planner i have gives 2D references.
+    # but of course otherwise the only reasonable
+    # thing is that the planner gives an SE3 reference
+    # to a MPC following an SE3 reference
+    if type(path_planner) == ProcessManager:
+        get_position = lambda robot: robot.T_w_e.translation[:2]
+    else:
+        get_position = lambda robot: robot.T_w_e
+    controlLoop = partial(
+        PathFollowingFromPlannerCtrllLoopTemplate,
+        path_planner,
+        get_position,
+        ocp,
+        CrocoEEPathFollowingMPCControlLoop,
+        args,
+        robot,
+    )
+    log_item = {
+        "qs": np.zeros(robot.model.nq),
+        "dqs": np.zeros(robot.model.nv),
+        "err_vec_ee": np.zeros(6),
+    }
+    save_past_item = {}
+    loop_manager = ControlLoopManager(
+        robot, controlLoop, args, save_past_item, log_item
+    )
+    if run:
+        loop_manager.run()
+    else:
+        return loop_manager
diff --git a/python/smc/control/optimal_control/croco_path_following/ocp/base_and_dual_arm_reference_ocp.py b/python/smc/control/optimal_control/croco_path_following/ocp/base_and_dual_arm_reference_ocp.py
new file mode 100644
index 0000000000000000000000000000000000000000..fd7784023032e67d2cf90f31dac7f57b9fcc50d1
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_path_following/ocp/base_and_dual_arm_reference_ocp.py
@@ -0,0 +1,40 @@
+from smc.control.optimal_control.croco_point_to_point.ocp.base_and_dual_arm_reference_ocp import (
+    BaseAndDualArmIKOCP,
+)
+from smc.robots.interfaces.whole_body_interface import (
+    DualArmWholeBodyInterface,
+)
+
+from argparse import Namespace
+
+
+class BaseAndDualArmEEPathFollowingOCP(BaseAndDualArmIKOCP):
+    def __init__(self, args: Namespace, robot: DualArmWholeBodyInterface, x0):
+        goal = robot.T_w_l, robot.T_w_r, robot.T_w_b.translation
+        super().__init__(args, robot, x0, goal)
+
+    def updateCosts(self, data):
+        path_base = data[0]
+        pathSE3_l = data[1]
+        pathSE3_r = data[2]
+        for i, runningModel in enumerate(self.solver.problem.runningModels):
+            runningModel.differential.costs.costs[
+                "base_translation" + str(i)
+            ].cost.residual.reference = path_base[i]
+            runningModel.differential.costs.costs[
+                "l_ee_pose" + str(i)
+            ].cost.residual.reference = pathSE3_l[i]
+            runningModel.differential.costs.costs[
+                "r_ee_pose" + str(i)
+            ].cost.residual.reference = pathSE3_r[i]
+
+        # idk if that's necessary
+        self.solver.problem.terminalModel.differential.costs.costs[
+            "base_translation" + str(self.args.n_knots)
+        ].cost.residual.reference = path_base[-1]
+        self.solver.problem.terminalModel.differential.costs.costs[
+            "l_ee_pose" + str(self.args.n_knots)
+        ].cost.residual.reference = pathSE3_l[-1]
+        self.solver.problem.terminalModel.differential.costs.costs[
+            "r_ee_pose" + str(self.args.n_knots)
+        ].cost.residual.reference = pathSE3_r[-1]
diff --git a/python/smc/control/optimal_control/croco_path_following/ocp/base_and_single_arm_reference_ocp.py b/python/smc/control/optimal_control/croco_path_following/ocp/base_and_single_arm_reference_ocp.py
new file mode 100644
index 0000000000000000000000000000000000000000..ef4072c25e1e47259cab993752734d63db9869b5
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_path_following/ocp/base_and_single_arm_reference_ocp.py
@@ -0,0 +1,41 @@
+from smc.control.optimal_control.croco_point_to_point.ocp.base_and_single_arm_reference_ocp import (
+    BaseAndSingleArmIKOCP,
+)
+from smc.robots.interfaces.whole_body_interface import (
+    SingleArmWholeBodyInterface,
+)
+
+from argparse import Namespace
+
+
+class BaseAndEEPathFollowingOCP(BaseAndSingleArmIKOCP):
+    """
+    createBaseAndEEPathFollowingOCP
+    -------------------------------
+    creates a path following problem.
+    it is instantiated to just to stay at the current position.
+    NOTE: the path MUST be time indexed with the SAME time used between the knots
+    """
+
+    def __init__(self, args: Namespace, robot: SingleArmWholeBodyInterface, x0):
+        goal = (robot.T_w_e, robot.T_w_b.translation.copy())
+        super().__init__(args, robot, x0, goal)
+
+    def updateCosts(self, data):
+        path_base = data[0]
+        pathSE3 = data[1]
+        for i, runningModel in enumerate(self.solver.problem.runningModels):
+            runningModel.differential.costs.costs[
+                "base_translation" + str(i)
+            ].cost.residual.reference = path_base[i]
+            runningModel.differential.costs.costs[
+                "ee_pose" + str(i)
+            ].cost.residual.reference = pathSE3[i]
+
+        # idk if that's necessary
+        self.solver.problem.terminalModel.differential.costs.costs[
+            "base_translation" + str(self.args.n_knots)
+        ].cost.residual.reference = path_base[-1]
+        self.solver.problem.terminalModel.differential.costs.costs[
+            "ee_pose" + str(self.args.n_knots)
+        ].cost.residual.reference = pathSE3[-1]
diff --git a/python/smc/control/optimal_control/croco_path_following/ocp/base_reference_ocp.py b/python/smc/control/optimal_control/croco_path_following/ocp/base_reference_ocp.py
new file mode 100644
index 0000000000000000000000000000000000000000..0de8d699687ed346293980b7d0967116916aa83d
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_path_following/ocp/base_reference_ocp.py
@@ -0,0 +1,32 @@
+from smc.control.optimal_control.croco_point_to_point.ocp.base_reference_ocp import (
+    BaseIKOCP,
+)
+from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface
+
+from argparse import Namespace
+
+
+class BasePathFollowingOCP(BaseIKOCP):
+    """
+    createBaseAndEEPathFollowingOCP
+    -------------------------------
+    creates a path following problem.
+    it is instantiated to just to stay at the current position.
+    NOTE: the path MUST be time indexed with the SAME time used between the knots
+    """
+
+    def __init__(self, args: Namespace, robot: MobileBaseInterface, x0):
+        goal = robot.T_w_b.translation.copy()
+        super().__init__(args, robot, x0, goal)
+
+    def updateCosts(self, data):
+        path_base = data
+        for i, runningModel in enumerate(self.solver.problem.runningModels):
+            runningModel.differential.costs.costs[
+                "base_translation" + str(i)
+            ].cost.residual.reference = path_base[i]
+
+        # idk if that's necessary
+        self.solver.problem.terminalModel.differential.costs.costs[
+            "base_translation" + str(self.args.n_knots)
+        ].cost.residual.reference = path_base[-1]
diff --git a/python/smc/control/optimal_control/croco_path_following/ocp/dual_arm_reference_ocp.py b/python/smc/control/optimal_control/croco_path_following/ocp/dual_arm_reference_ocp.py
new file mode 100644
index 0000000000000000000000000000000000000000..e9edab741ee4aeb947c40d97ca909c471f63eb1c
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_path_following/ocp/dual_arm_reference_ocp.py
@@ -0,0 +1,30 @@
+from smc.control.optimal_control.croco_point_to_point.ocp.dual_arm_reference_ocp import (
+    DualArmIKOCP,
+)
+from smc.robots.interfaces.dual_arm_interface import DualArmInterface
+
+from argparse import Namespace
+
+
+class DualArmEEPathFollowingOCP(DualArmIKOCP):
+    def __init__(self, args: Namespace, robot: DualArmInterface, x0):
+        goal = (robot.T_w_l, robot.T_w_r)
+        super().__init__(args, robot, x0, goal)
+
+    def updateCosts(self, data):
+        pathSE3_l = data[0]
+        pathSE3_r = data[1]
+        for i, runningModel in enumerate(self.solver.problem.runningModels):
+            runningModel.differential.costs.costs[
+                "l_ee_pose" + str(i)
+            ].cost.residual.reference = pathSE3_l[i]
+            runningModel.differential.costs.costs[
+                "r_ee_pose" + str(i)
+            ].cost.residual.reference = pathSE3_r[i]
+
+        self.solver.problem.terminalModel.differential.costs.costs[
+            "l_ee_pose" + str(self.args.n_knots)
+        ].cost.residual.reference = pathSE3_l[-1]
+        self.solver.problem.terminalModel.differential.costs.costs[
+            "r_ee_pose" + str(self.args.n_knots)
+        ].cost.residual.reference = pathSE3_r[-1]
diff --git a/python/smc/control/optimal_control/croco_path_following/ocp/single_arm_reference_ocp.py b/python/smc/control/optimal_control/croco_path_following/ocp/single_arm_reference_ocp.py
new file mode 100644
index 0000000000000000000000000000000000000000..892553fe4a55a8dd8ffe70338e25bc592fd1f01c
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_path_following/ocp/single_arm_reference_ocp.py
@@ -0,0 +1,31 @@
+from smc.control.optimal_control.croco_point_to_point.ocp.single_arm_reference_ocp import (
+    SingleArmIKOCP,
+)
+from smc.robots.interfaces.single_arm_interface import SingleArmInterface
+
+import numpy as np
+from argparse import Namespace
+
+
+class CrocoEEPathFollowingOCP(SingleArmIKOCP):
+    """
+    createCrocoEEPathFollowingOCP
+    -------------------------------
+    creates a path following problem with a single end-effector reference.
+    it is instantiated to just to stay at the current position.
+    NOTE: the path MUST be time indexed with the SAME time used between the knots
+    """
+
+    def __init__(self, args: Namespace, robot: SingleArmInterface, x0: np.ndarray):
+        goal = robot.T_w_e
+        super().__init__(args, robot, x0, goal)
+
+    def updateCosts(self, data):
+        for i, runningModel in enumerate(self.solver.problem.runningModels):
+            runningModel.differential.costs.costs[
+                "ee_pose" + str(i)
+            ].cost.residual.reference = data[i]
+
+        self.solver.problem.terminalModel.differential.costs.costs[
+            "ee_pose" + str(self.args.n_knots)
+        ].cost.residual.reference = data[-1]
diff --git a/python/smc/control/optimal_control/croco_point_to_point/mpc/base_and_dual_arm_reference_mpc.py b/python/smc/control/optimal_control/croco_point_to_point/mpc/base_and_dual_arm_reference_mpc.py
new file mode 100644
index 0000000000000000000000000000000000000000..9dd11edf7d61e6663d2efc93809a49595d08e631
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_point_to_point/mpc/base_and_dual_arm_reference_mpc.py
@@ -0,0 +1,94 @@
+from smc.control.controller_templates.point_to_point import (
+    DualEEAndBaseP2PCtrlLoopTemplate,
+)
+from smc.robots.interfaces.whole_body_interface import (
+    DualArmWholeBodyInterface,
+)
+from smc.control.optimal_control.abstract_croco_ocp import CrocoOCP
+from smc.control.optimal_control.croco_point_to_point.ocp.base_and_dual_arm_reference_ocp import (
+    BaseAndDualArmIKOCP,
+)
+from smc.control.control_loop_manager import ControlLoopManager
+
+import pinocchio as pin
+import numpy as np
+from functools import partial
+from collections import deque
+from argparse import Namespace
+
+
+def CrocoP2PDualEEAndBaseMPCControlLoop(
+    ocp,
+    T_w_absgoal: pin.SE3,
+    T_absgoal_l: pin.SE3,
+    T_absgoal_r: pin.SE3,
+    p_basegoal: np.ndarray,
+    args: Namespace,
+    robot: DualArmWholeBodyInterface,
+    t: int,
+    past_data: dict[str, deque[np.ndarray]],
+) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
+    """
+    CrocoP2PEEAndBaseMPCControlLoop
+    ---------------------
+    """
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp.warmstartAndReSolve(x0)
+    xs = np.array(ocp.solver.xs)
+    # NOTE: for some reason the first value is always some wild bs
+    v_cmd = xs[1, robot.model.nq :]
+
+    return v_cmd, {}, {}
+
+
+def CrocoDualEEAndBaseP2PMPC(
+    args: Namespace,
+    robot: DualArmWholeBodyInterface,
+    T_w_absgoal: pin.SE3,
+    T_absgoal_l: pin.SE3,
+    T_absgoal_r: pin.SE3,
+    p_basegoal: np.ndarray,
+    run=True,
+) -> None | ControlLoopManager:
+    """
+    IKMPC
+    -----
+    run mpc for a point-to-point inverse kinematics.
+    note that the actual problem is solved on
+    a dynamics level, and velocities we command
+    are actually extracted from the state x(q,dq)
+    """
+    x0 = np.concatenate([robot.q, robot.v])
+    T_w_lgoal = T_absgoal_l.act(T_w_absgoal)
+    T_w_rgoal = T_absgoal_r.act(T_w_absgoal)
+    goal = (T_w_lgoal, T_w_rgoal, p_basegoal)
+    ocp = BaseAndDualArmIKOCP(args, robot, x0, goal)
+    ocp.solveInitialOCP(x0)
+
+    controlLoop = partial(
+        DualEEAndBaseP2PCtrlLoopTemplate,
+        ocp,
+        T_w_absgoal,
+        T_absgoal_l,
+        T_absgoal_r,
+        p_basegoal,
+        CrocoP2PDualEEAndBaseMPCControlLoop,
+        args,
+        robot,
+    )
+    log_item = {
+        "qs": np.zeros(robot.nq),
+        "dqs": np.zeros(robot.nv),
+        "dqs_cmd": np.zeros(robot.nv),
+        "l_err_norm": np.zeros(1),
+        "r_err_norm": np.zeros(1),
+        "base_err_norm": np.zeros(1),
+    }
+    save_past_dict = {}
+    loop_manager = ControlLoopManager(
+        robot, controlLoop, args, save_past_dict, log_item
+    )
+    if run:
+        loop_manager.run()
+    else:
+        return loop_manager
diff --git a/python/smc/control/optimal_control/croco_point_to_point/mpc/base_and_single_arm_reference_mpc.py b/python/smc/control/optimal_control/croco_point_to_point/mpc/base_and_single_arm_reference_mpc.py
new file mode 100644
index 0000000000000000000000000000000000000000..2c6a411c70858c8649601a2a57f0d3605a8dd906
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_point_to_point/mpc/base_and_single_arm_reference_mpc.py
@@ -0,0 +1,81 @@
+from smc.control.controller_templates.point_to_point import (
+    EEAndBaseP2PCtrlLoopTemplate,
+)
+from smc.robots.interfaces.whole_body_interface import (
+    SingleArmWholeBodyInterface,
+)
+from smc.control.optimal_control.abstract_croco_ocp import CrocoOCP
+from smc.control.optimal_control.croco_point_to_point.ocp.base_and_single_arm_reference_ocp import (
+    BaseAndSingleArmIKOCP,
+)
+from smc.control.control_loop_manager import ControlLoopManager
+
+import pinocchio as pin
+import numpy as np
+from functools import partial
+from collections import deque
+from argparse import Namespace
+
+
+def CrocoP2PEEAndBaseMPCControlLoop(
+    ocp: CrocoOCP,
+    T_w_eegoal: pin.SE3,
+    p_basegoal: np.ndarray,
+    args: Namespace,
+    robot: SingleArmWholeBodyInterface,
+    t: int,
+    past_data: dict[str, deque[np.ndarray]],
+) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
+    """
+    CrocoP2PEEAndBaseMPCControlLoop
+    ---------------------
+    """
+    # set initial state from sensor
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp.warmstartAndReSolve(x0)
+    xs = np.array(ocp.solver.xs)
+    # NOTE: for some reason the first value is always some wild bs
+    vel_cmd = xs[1, robot.model.nq :]
+    return vel_cmd, {}, {}
+
+
+def CrocoEEAndBaseP2PMPC(
+    args, robot, T_w_eegoal: pin.SE3, p_basegoal: np.ndarray, run=True
+):
+    """
+    IKMPC
+    -----
+    run mpc for a point-to-point inverse kinematics.
+    note that the actual problem is solved on
+    a dynamics level, and velocities we command
+    are actually extracted from the state x(q,dq)
+    """
+    x0 = np.concatenate([robot.q, robot.v])
+    goal = (T_w_eegoal, p_basegoal)
+    ocp = BaseAndSingleArmIKOCP(args, robot, x0, goal)
+    ocp.solveInitialOCP(x0)
+
+    controlLoop = partial(
+        EEAndBaseP2PCtrlLoopTemplate,
+        ocp,
+        T_w_eegoal,
+        p_basegoal,
+        CrocoP2PEEAndBaseMPCControlLoop,
+        args,
+        robot,
+    )
+    log_item = {
+        "qs": np.zeros(robot.nq),
+        "dqs": np.zeros(robot.nv),
+        "dqs_cmd": np.zeros(robot.nv),
+        "ee_err_norm": np.zeros(1),
+        "base_err_norm": np.zeros(1),
+    }
+    save_past_dict = {}
+    loop_manager = ControlLoopManager(
+        robot, controlLoop, args, save_past_dict, log_item
+    )
+    if run:
+        loop_manager.run()
+    else:
+        return loop_manager
diff --git a/python/smc/control/optimal_control/croco_point_to_point/mpc/base_reference_mpc.py b/python/smc/control/optimal_control/croco_point_to_point/mpc/base_reference_mpc.py
new file mode 100644
index 0000000000000000000000000000000000000000..0399371fad1fffe53c10dfc6014f142740cb6da5
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_point_to_point/mpc/base_reference_mpc.py
@@ -0,0 +1,72 @@
+from smc.control.controller_templates.point_to_point import (
+    BaseP2PCtrlLoopTemplate,
+)
+from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface
+from smc.control.optimal_control.abstract_croco_ocp import CrocoOCP
+from smc.control.optimal_control.croco_point_to_point.ocp.base_reference_ocp import (
+    BaseIKOCP,
+)
+from smc.control.control_loop_manager import ControlLoopManager
+
+import pinocchio as pin
+import numpy as np
+from functools import partial
+from collections import deque
+from argparse import Namespace
+
+
+def CrocoBaseP2PMPCControlLoop(
+    ocp: CrocoOCP,
+    p_basegoal: np.ndarray,
+    args: Namespace,
+    robot: MobileBaseInterface,
+    _: int,
+    __: dict[str, deque[np.ndarray]],
+) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
+    """
+    CrocoIKMPCControlLoop
+    ---------------------
+    """
+    # set initial state from sensor
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp.warmstartAndReSolve(x0)
+    xs = np.array(ocp.solver.xs)
+    # NOTE: for some reason the first value is always some wild bs
+    vel_cmd = xs[1, robot.model.nq :]
+    return vel_cmd, {}, {}
+
+
+def CrocoBaseP2PMPC(
+    args: Namespace, robot: MobileBaseInterface, p_basegoal: np.ndarray, run=True
+):
+    """
+    CrocoBaseP2PMPC
+    -----
+    base point-to-point task
+    """
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp = BaseIKOCP(args, robot, x0, p_basegoal)
+    ocp.solveInitialOCP(x0)
+
+    controlLoop = partial(
+        BaseP2PCtrlLoopTemplate,
+        ocp,
+        p_basegoal,
+        CrocoBaseP2PMPCControlLoop,
+        args,
+        robot,
+    )
+    log_item = {
+        "qs": np.zeros(robot.nq),
+        "base_err_norm": np.zeros(1),
+        "dqs": np.zeros(robot.nv),
+        "dqs_cmd": np.zeros(robot.nv),
+    }
+    save_past_dict = {}
+    loop_manager = ControlLoopManager(
+        robot, controlLoop, args, save_past_dict, log_item
+    )
+    if run:
+        loop_manager.run()
+    else:
+        return loop_manager
diff --git a/python/smc/control/optimal_control/croco_point_to_point/mpc/dual_arm_reference_mpc.py b/python/smc/control/optimal_control/croco_point_to_point/mpc/dual_arm_reference_mpc.py
new file mode 100644
index 0000000000000000000000000000000000000000..8972eb3f51d61a87481e5e564624dadd24712194
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_point_to_point/mpc/dual_arm_reference_mpc.py
@@ -0,0 +1,109 @@
+from smc.control.controller_templates.point_to_point import (
+    DualEEP2PCtrlLoopTemplate,
+)
+from smc.robots.interfaces.dual_arm_interface import DualArmInterface
+from smc.control.optimal_control.abstract_croco_ocp import CrocoOCP
+from smc.control.optimal_control.croco_point_to_point.ocp.dual_arm_reference_ocp import (
+    DualArmIKOCP,
+)
+from smc.control.control_loop_manager import ControlLoopManager
+
+import pinocchio as pin
+import numpy as np
+from functools import partial
+from collections import deque
+from argparse import Namespace
+
+from IPython import embed
+
+
+def CrocoDualEEP2PMPCControlLoop(
+    ocp: CrocoOCP,
+    T_w_absgoal: pin.SE3,
+    T_absgoal_l: pin.SE3,
+    T_absgoal_r: pin.SE3,
+    args: Namespace,
+    robot: DualArmInterface,
+    t: int,
+    past_data: dict[str, deque[np.ndarray]],
+) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
+    """
+    CrocoDualEEP2PMPCControlLoop
+    ---------------------
+    """
+    # set initial state from sensor
+    x0 = np.concatenate([robot.q, robot.v])
+
+    T_w_lgoal = T_absgoal_l.act(T_w_absgoal)
+    T_w_rgoal = T_absgoal_r.act(T_w_absgoal)
+    SEerror_left = robot.T_w_l.actInv(T_w_lgoal)
+    SEerror_right = robot.T_w_r.actInv(T_w_rgoal)
+
+    # update costs depending on goal proximity
+    err_vector_left = pin.log6(SEerror_left).vector
+    err_vector_right = pin.log6(SEerror_right).vector
+    err_vector_left_norm = np.linalg.norm(err_vector_left)
+    err_vector_right_norm = np.linalg.norm(err_vector_right)
+
+    # completely arbitrary numbers in condition
+    if (err_vector_left_norm < 0.7) and (err_vector_right_norm < 0.7):
+        ocp.terminalCostModel.changeCostStatus("velFinal_l", True)
+        ocp.terminalCostModel.changeCostStatus("velFinal_r", True)
+
+    ocp.warmstartAndReSolve(x0)
+    xs = np.array(ocp.solver.xs)
+    # NOTE: for some reason the first value is always some wild bs
+    vel_cmd = xs[1, robot.model.nq :]
+    return vel_cmd, {}, {}
+
+
+def CrocoDualEEP2PMPC(
+    args: Namespace,
+    robot: DualArmInterface,
+    T_w_absgoal: pin.SE3,
+    T_absgoal_l: pin.SE3,
+    T_absgoal_r: pin.SE3,
+    run=True,
+):
+    """
+    DualEEP2PMPC
+    -----
+    run mpc for a point-to-point inverse kinematics.
+    note that the actual problem is solved on
+    a dynamics level, and velocities we command
+    are actually extracted from the state x(q,dq)
+    """
+    x0 = np.concatenate([robot.q, robot.v])
+    T_w_lgoal = T_absgoal_l.act(T_w_absgoal)
+    T_w_rgoal = T_absgoal_r.act(T_w_absgoal)
+
+    ocp = DualArmIKOCP(args, robot, x0, (T_w_lgoal, T_w_rgoal))
+    ocp.terminalCostModel.changeCostStatus("velFinal_l", False)
+    ocp.terminalCostModel.changeCostStatus("velFinal_r", False)
+    ocp.solveInitialOCP(x0)
+
+    controlLoop = partial(
+        DualEEP2PCtrlLoopTemplate,
+        ocp,
+        T_w_absgoal,
+        T_absgoal_l,
+        T_absgoal_r,
+        CrocoDualEEP2PMPCControlLoop,
+        args,
+        robot,
+    )
+    log_item = {
+        "qs": np.zeros(robot.nq),
+        "dqs": np.zeros(robot.nv),
+        "dqs_cmd": np.zeros(robot.nv),
+        "l_err_norm": np.zeros(1),
+        "r_err_norm": np.zeros(1),
+    }
+    save_past_dict = {}
+    loop_manager = ControlLoopManager(
+        robot, controlLoop, args, save_past_dict, log_item
+    )
+    if run:
+        loop_manager.run()
+    else:
+        return loop_manager
diff --git a/python/smc/control/optimal_control/croco_point_to_point/mpc/single_arm_reference_mpc.py b/python/smc/control/optimal_control/croco_point_to_point/mpc/single_arm_reference_mpc.py
new file mode 100644
index 0000000000000000000000000000000000000000..bc459a1508693f01a2d2998595556bea9da78db6
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_point_to_point/mpc/single_arm_reference_mpc.py
@@ -0,0 +1,70 @@
+from smc.control.controller_templates.point_to_point import (
+    EEP2PCtrlLoopTemplate,
+)
+from smc.robots.interfaces.single_arm_interface import SingleArmInterface
+from smc.control.optimal_control.abstract_croco_ocp import CrocoOCP
+from smc.control.optimal_control.croco_point_to_point.ocp.single_arm_reference_ocp import (
+    SingleArmIKOCP,
+)
+from smc.control.control_loop_manager import ControlLoopManager
+
+import pinocchio as pin
+import numpy as np
+from functools import partial
+from collections import deque
+from argparse import Namespace
+
+
+def CrocoEEP2PMPCControlLoop(
+    ocp: CrocoOCP,
+    T_w_goal: pin.SE3,
+    args: Namespace,
+    robot: SingleArmInterface,
+    _: int,
+    __: dict[str, deque[np.ndarray]],
+) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
+    """
+    CrocoIKMPCControlLoop
+    ---------------------
+    """
+    # set initial state from sensor
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp.warmstartAndReSolve(x0)
+    xs = np.array(ocp.solver.xs)
+    # NOTE: for some reason the first value is always some wild bs
+    vel_cmd = xs[1, robot.model.nq :]
+    return vel_cmd, {}, {}
+
+
+def CrocoEEP2PMPC(
+    args: Namespace, robot: SingleArmInterface, T_w_goal: pin.SE3, run=True
+):
+    """
+    IKMPC
+    -----
+    run mpc for a point-to-point inverse kinematics.
+    note that the actual problem is solved on
+    a dynamics level, and velocities we command
+    are actually extracted from the state x(q,dq)
+    """
+    x0 = np.concatenate([robot.q, robot.v])
+    ocp = SingleArmIKOCP(args, robot, x0, T_w_goal)
+    ocp.solveInitialOCP(x0)
+
+    controlLoop = partial(
+        EEP2PCtrlLoopTemplate, ocp, T_w_goal, CrocoEEP2PMPCControlLoop, args, robot
+    )
+    log_item = {
+        "qs": np.zeros(robot.nq),
+        "err_norm": np.zeros(1),
+        "dqs": np.zeros(robot.nv),
+        "dqs_cmd": np.zeros(robot.nv),
+    }
+    save_past_dict = {}
+    loop_manager = ControlLoopManager(
+        robot, controlLoop, args, save_past_dict, log_item
+    )
+    if run:
+        loop_manager.run()
+    else:
+        return loop_manager
diff --git a/python/smc/control/optimal_control/croco_point_to_point/ocp/base_and_dual_arm_reference_ocp.py b/python/smc/control/optimal_control/croco_point_to_point/ocp/base_and_dual_arm_reference_ocp.py
new file mode 100644
index 0000000000000000000000000000000000000000..44c342f6daa0f6500e13b4c38022e763c1a9bc50
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_point_to_point/ocp/base_and_dual_arm_reference_ocp.py
@@ -0,0 +1,53 @@
+from smc.robots.interfaces.whole_body_interface import (
+    DualArmWholeBodyInterface,
+)
+from smc.control.optimal_control.croco_point_to_point.ocp.base_reference_ocp import (
+    BaseIKOCP,
+)
+from smc.control.optimal_control.croco_point_to_point.ocp.dual_arm_reference_ocp import (
+    DualArmIKOCP,
+)
+
+import numpy as np
+from argparse import Namespace
+
+
+class BaseAndDualArmIKOCP(DualArmIKOCP, BaseIKOCP):
+    def __init__(
+        self,
+        args: Namespace,
+        robot: DualArmWholeBodyInterface,
+        x0: np.ndarray,
+        goal,
+    ):
+        super().__init__(args, robot, x0, goal)
+
+    def constructTaskCostsValues(self):
+        self.base_translation_cost_values = np.linspace(
+            self.args.base_translation_cost,
+            self.args.base_translation_cost
+            * self.args.linearly_increasing_task_cost_factor,
+            self.args.n_knots + 1,
+        )
+        self.ee_pose_cost_values = np.linspace(
+            self.args.ee_pose_cost,
+            self.args.ee_pose_cost * self.args.linearly_increasing_task_cost_factor,
+            self.args.n_knots + 1,
+        )
+
+    def constructTaskObjectiveFunction(
+        self,
+        goal,
+    ) -> None:
+        T_w_lgoal, T_w_rgoal, p_basegoal = goal
+        super().constructTaskObjectiveFunction((T_w_lgoal, T_w_rgoal))
+        BaseIKOCP.constructTaskObjectiveFunction(self, p_basegoal)
+
+    # there is nothing to update in a point-to-point task
+    def updateCosts(self, data) -> None:
+        pass
+
+    def updateGoalInModels(self, goal) -> None:
+        T_w_lgoal, T_w_rgoal, p_basegoal = goal
+        super().updateGoalInModels((T_w_lgoal, T_w_rgoal))
+        BaseIKOCP.updateGoalInModels(self, p_basegoal)
diff --git a/python/smc/control/optimal_control/croco_point_to_point/ocp/base_and_single_arm_reference_ocp.py b/python/smc/control/optimal_control/croco_point_to_point/ocp/base_and_single_arm_reference_ocp.py
new file mode 100644
index 0000000000000000000000000000000000000000..75f4d1c0609628d5369c285ba09248af6b6f35bf
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_point_to_point/ocp/base_and_single_arm_reference_ocp.py
@@ -0,0 +1,54 @@
+from smc.robots.interfaces.whole_body_interface import (
+    SingleArmWholeBodyInterface,
+)
+from smc.control.optimal_control.croco_point_to_point.ocp.base_reference_ocp import (
+    BaseIKOCP,
+)
+from smc.control.optimal_control.croco_point_to_point.ocp.single_arm_reference_ocp import (
+    SingleArmIKOCP,
+)
+
+import numpy as np
+from argparse import Namespace
+
+
+class BaseAndSingleArmIKOCP(SingleArmIKOCP, BaseIKOCP):
+    def __init__(
+        self,
+        args: Namespace,
+        robot: SingleArmWholeBodyInterface,
+        x0: np.ndarray,
+        goal,
+    ):
+        super().__init__(args, robot, x0, goal)
+
+    def constructTaskCostsValues(self):
+        self.base_translation_cost_values = np.linspace(
+            self.args.base_translation_cost,
+            self.args.base_translation_cost
+            * self.args.linearly_increasing_task_cost_factor,
+            self.args.n_knots + 1,
+        )
+        self.ee_pose_cost_values = np.linspace(
+            self.args.ee_pose_cost,
+            self.args.ee_pose_cost * self.args.linearly_increasing_task_cost_factor,
+            self.args.n_knots + 1,
+        )
+
+    def constructTaskObjectiveFunction(
+        self,
+        goal,
+    ) -> None:
+        T_w_eegoal, p_basegoal = goal
+        super().constructTaskObjectiveFunction(T_w_eegoal)
+        BaseIKOCP.constructTaskObjectiveFunction(self, p_basegoal)
+
+    # there is nothing to update in a point-to-point task
+    def updateCosts(self, data):
+        pass
+
+    def updateGoalInModels(self, goal) -> None:
+        # self, T_w_eegoal: pin.SE3, p_basegoal: np.ndarray) -> None:
+        T_w_eegoal, p_basegoal = goal
+        super().updateGoalInModels(T_w_eegoal)
+        BaseIKOCP.updateGoalInModels(self, p_basegoal)
diff --git a/python/smc/control/optimal_control/croco_point_to_point/ocp/base_reference_ocp.py b/python/smc/control/optimal_control/croco_point_to_point/ocp/base_reference_ocp.py
new file mode 100644
index 0000000000000000000000000000000000000000..086b5e3684942bcc0c0db4409a17b46ac6fbb282
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_point_to_point/ocp/base_reference_ocp.py
@@ -0,0 +1,60 @@
+from smc.control.optimal_control.abstract_croco_ocp import CrocoOCP
+from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface
+
+import numpy as np
+from pinocchio import SE3
+import crocoddyl
+from argparse import Namespace
+
+
+class BaseIKOCP(CrocoOCP):
+    def __init__(
+        self,
+        args: Namespace,
+        robot: MobileBaseInterface,
+        x0: np.ndarray,
+        p_basegoal: SE3,
+    ):
+        super().__init__(args, robot, x0, p_basegoal)
+
+    def constructTaskCostsValues(self):
+        self.base_translation_cost_values = np.linspace(
+            self.args.base_translation_cost,
+            self.args.base_translation_cost
+            * self.args.linearly_increasing_task_cost_factor,
+            self.args.n_knots + 1,
+        )
+
+    def constructTaskObjectiveFunction(self, goal) -> None:
+        p_basegoal = goal
+        for i in range(self.args.n_knots):
+            baseTranslationResidual = crocoddyl.ResidualModelFrameTranslation(
+                self.state, self.robot.base_frame_id, p_basegoal, self.state.nv
+            )
+            baseTrackingCost = crocoddyl.CostModelResidual(
+                self.state, baseTranslationResidual
+            )
+            self.runningCostModels[i].addCost(
+                "base_translation" + str(i),
+                baseTrackingCost,
+                self.base_translation_cost_values[i],
+            )
+        self.terminalCostModel.addCost(
+            "base_translation" + str(self.args.n_knots),
+            baseTrackingCost,
+            self.base_translation_cost_values[-1],
+        )
+
+    # there is nothing to update in a point-to-point task
+    def updateCosts(self, data):
+        pass
+
+    def updateGoalInModels(self, goal) -> None:
+        p_basegoal = goal
+        for i, runningModel in enumerate(self.solver.problem.runningModels):
+            runningModel.differential.costs.costs[
+                "base_translation" + str(i)
+            ].cost.residual.reference = p_basegoal
+        self.solver.problem.terminalModel.differential.costs.costs[
+            "base_translation" + str(self.args.n_knots)
+        ].cost.residual.reference = p_basegoal
diff --git a/python/smc/control/optimal_control/croco_point_to_point/ocp/dual_arm_reference_ocp.py b/python/smc/control/optimal_control/croco_point_to_point/ocp/dual_arm_reference_ocp.py
new file mode 100644
index 0000000000000000000000000000000000000000..dd3f39ae3d22c054ed8a63ca580bfe1bafe57564
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_point_to_point/ocp/dual_arm_reference_ocp.py
@@ -0,0 +1,109 @@
+from smc.control.optimal_control.abstract_croco_ocp import CrocoOCP
+from smc.robots.interfaces.dual_arm_interface import DualArmInterface
+
+import numpy as np
+import pinocchio as pin
+import crocoddyl
+from argparse import Namespace
+
+
+class DualArmIKOCP(CrocoOCP):
+    def __init__(
+        self, args: Namespace, robot: DualArmInterface, x0: np.ndarray, goal: pin.SE3
+    ):
+        super().__init__(args, robot, x0, goal)
+
+    def constructTaskCostsValues(self):
+        self.ee_pose_cost_values = np.linspace(
+            self.args.ee_pose_cost,
+            self.args.ee_pose_cost * self.args.linearly_increasing_task_cost_factor,
+            self.args.n_knots + 1,
+        )
+
+    def constructTaskObjectiveFunction(self, goal) -> None:
+        T_w_lgoal, T_w_rgoal = goal
+        framePlacementResidual_l = crocoddyl.ResidualModelFramePlacement(
+            self.state,
+            self.robot.l_ee_frame_id,
+            T_w_lgoal.copy(),
+            self.state.nv,
+        )
+        framePlacementResidual_r = crocoddyl.ResidualModelFramePlacement(
+            self.state,
+            self.robot.r_ee_frame_id,
+            T_w_rgoal.copy(),
+            self.state.nv,
+        )
+        goalTrackingCost_l = crocoddyl.CostModelResidual(
+            self.state, framePlacementResidual_l
+        )
+        goalTrackingCost_r = crocoddyl.CostModelResidual(
+            self.state, framePlacementResidual_r
+        )
+        # TODO: final velocity costs only make sense if you're running a single ocp, but not mpc!!
+        # TODO: have an argument or something to include or not include them!
+
+        if self.args.stop_at_final:
+            frameVelocityResidual_l = crocoddyl.ResidualModelFrameVelocity(
+                self.state,
+                self.robot.l_ee_frame_id,
+                pin.Motion(np.zeros(6)),
+                pin.ReferenceFrame.WORLD,
+            )
+            frameVelocityCost_l = crocoddyl.CostModelResidual(
+                self.state, frameVelocityResidual_l
+            )
+            frameVelocityResidual_r = crocoddyl.ResidualModelFrameVelocity(
+                self.state,
+                self.robot.r_ee_frame_id,
+                pin.Motion(np.zeros(6)),
+                pin.ReferenceFrame.WORLD,
+            )
+            frameVelocityCost_r = crocoddyl.CostModelResidual(
+                self.state, frameVelocityResidual_r
+            )
+        for i in range(self.args.n_knots):
+            self.runningCostModels[i].addCost(
+                "l_ee_pose" + str(i),
+                goalTrackingCost_l,
+                self.ee_pose_cost_values[i],
+            )
+            self.runningCostModels[i].addCost(
+                "r_ee_pose" + str(i),
+                goalTrackingCost_r,
+                self.ee_pose_cost_values[i],
+            )
+        self.terminalCostModel.addCost(
+            "l_ee_pose" + str(self.args.n_knots),
+            goalTrackingCost_l,
+            self.ee_pose_cost_values[-1],
+        )
+        self.terminalCostModel.addCost(
+            "r_ee_pose" + str(self.args.n_knots),
+            goalTrackingCost_r,
+            self.ee_pose_cost_values[-1],
+        )
+        if self.args.stop_at_final:
+            self.terminalCostModel.addCost("velFinal_l", frameVelocityCost_l, 1e3)
+            self.terminalCostModel.addCost("velFinal_r", frameVelocityCost_r, 1e3)
+
+    # there is nothing to update in a point-to-point task
+    def updateCosts(self, data):
+        pass
+
+    def updateGoalInModels(self, goal) -> None:
+        T_w_lgoal, T_w_rgoal = goal
+        for i, runningModel in enumerate(self.solver.problem.runningModels):
+            runningModel.differential.costs.costs[
+                "l_ee_pose" + str(i)
+            ].cost.residual.reference = T_w_lgoal
+            runningModel.differential.costs.costs[
+                "r_ee_pose" + str(i)
+            ].cost.residual.reference = T_w_rgoal
+
+        self.solver.problem.terminalModel.differential.costs.costs[
+            "l_ee_pose" + str(self.args.n_knots)
+        ].cost.residual.reference = T_w_lgoal
+        self.solver.problem.terminalModel.differential.costs.costs[
+            "r_ee_pose" + str(self.args.n_knots)
+        ].cost.residual.reference = T_w_rgoal
diff --git a/python/smc/control/optimal_control/croco_point_to_point/ocp/single_arm_reference_ocp.py b/python/smc/control/optimal_control/croco_point_to_point/ocp/single_arm_reference_ocp.py
new file mode 100644
index 0000000000000000000000000000000000000000..08ab02cd9ed2d22e035b78139d2b937862de8da9
--- /dev/null
+++ b/python/smc/control/optimal_control/croco_point_to_point/ocp/single_arm_reference_ocp.py
@@ -0,0 +1,72 @@
+from smc.control.optimal_control.abstract_croco_ocp import CrocoOCP
+from smc.robots.interfaces.single_arm_interface import SingleArmInterface
+
+import numpy as np
+import pinocchio as pin
+import crocoddyl
+from argparse import Namespace
+
+
+class SingleArmIKOCP(CrocoOCP):
+    def __init__(
+        self,
+        args: Namespace,
+        robot: SingleArmInterface,
+        x0: np.ndarray,
+        T_w_goal: pin.SE3,
+    ):
+        super().__init__(args, robot, x0, T_w_goal)
+
+    def constructTaskCostsValues(self):
+        self.ee_pose_cost_values = np.linspace(
+            self.args.ee_pose_cost,
+            self.args.ee_pose_cost * self.args.linearly_increasing_task_cost_factor,
+            self.args.n_knots + 1,
+        )
+
+    def constructTaskObjectiveFunction(self, goal) -> None:
+        T_w_goal = goal
+        framePlacementResidual = crocoddyl.ResidualModelFramePlacement(
+            self.state,
+            self.robot.ee_frame_id,
+            T_w_goal.copy(),
+            self.state.nv,
+        )
+        goalTrackingCost = crocoddyl.CostModelResidual(
+            self.state, framePlacementResidual
+        )
+        # TODO: final velocity costs only make sense if you're running a single ocp, but not mpc!!
+        # TODO: have an argument or something to include or not include them!
+        # frameVelocityResidual = crocoddyl.ResidualModelFrameVelocity(
+        #    self.state,
+        #    self.robot.ee_frame_id,
+        #    pin.Motion(np.zeros(6)),
+        #    pin.ReferenceFrame.WORLD,
+        # )
+        # frameVelocityCost = crocoddyl.CostModelResidual(
+        #    self.state, frameVelocityResidual
+        # )
+        for i in range(self.args.n_knots):
+            self.runningCostModels[i].addCost(
+                "ee_pose" + str(i), goalTrackingCost, self.ee_pose_cost_values[i]
+            )
+        self.terminalCostModel.addCost(
+            "ee_pose" + str(self.args.n_knots),
+            goalTrackingCost,
+            self.ee_pose_cost_values[-1],
+        )
+        # self.terminalCostModel.addCost("velFinal", frameVelocityCost, 1e3)
+
+    # there is nothing to update in a point-to-point task
+    def updateCosts(self, data):
+        pass
+
+    def updateGoalInModels(self, goal) -> None:
+        T_w_goal = goal
+        for i, runningModel in enumerate(self.solver.problem.runningModels):
+            runningModel.differential.costs.costs[
+                "ee_pose" + str(i)
+            ].cost.residual.reference = T_w_goal
+        self.solver.problem.terminalModel.differential.costs.costs[
+            "ee_pose" + str(self.args.n_knots)
+        ].cost.residual.reference = T_w_goal
diff --git a/python/smc/control/optimal_control/path_following_croco_ocp.py b/python/smc/control/optimal_control/path_following_croco_ocp.py
deleted file mode 100644
index 1b9db978f172dad1b4b07a7befc62da6f7f90e7e..0000000000000000000000000000000000000000
--- a/python/smc/control/optimal_control/path_following_croco_ocp.py
+++ /dev/null
@@ -1,159 +0,0 @@
-from smc.control.optimal_control.point_to_point_croco_ocp import (
-    BaseIKOCP,
-    SingleArmIKOCP,
-    DualArmIKOCP,
-    BaseAndSingleArmIKOCP,
-    BaseAndDualArmIKOCP,
-)
-from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface
-from smc.robots.interfaces.single_arm_interface import SingleArmInterface
-from smc.robots.interfaces.dual_arm_interface import DualArmInterface
-from smc.robots.interfaces.whole_body_interface import (
-    SingleArmWholeBodyInterface,
-    DualArmWholeBodyInterface,
-)
-
-import numpy as np
-from argparse import Namespace
-
-
-# NOTE: all of these should be rewritten so that it inherits from BaseIKOCP,
-# and then you just override update goal.
-# you'd cut this code in less than half
-class BasePathFollowingOCP(BaseIKOCP):
-    """
-    createBaseAndEEPathFollowingOCP
-    -------------------------------
-    creates a path following problem.
-    it is instantiated to just to stay at the current position.
-    NOTE: the path MUST be time indexed with the SAME time used between the knots
-    """
-
-    def __init__(self, args, robot: MobileBaseInterface, x0):
-        goal = robot.T_w_b.translation.copy()
-        super().__init__(args, robot, x0, goal)
-
-    def updateCosts(self, data):
-        path_base = data
-        for i, runningModel in enumerate(self.solver.problem.runningModels):
-            runningModel.differential.costs.costs[
-                "base_translation" + str(i)
-            ].cost.residual.reference = path_base[i]
-
-        # idk if that's necessary
-        self.solver.problem.terminalModel.differential.costs.costs[
-            "base_translation" + str(self.args.n_knots)
-        ].cost.residual.reference = path_base[-1]
-
-
-class CrocoEEPathFollowingOCP(SingleArmIKOCP):
-    """
-    createCrocoEEPathFollowingOCP
-    -------------------------------
-    creates a path following problem with a single end-effector reference.
-    it is instantiated to just to stay at the current position.
-    NOTE: the path MUST be time indexed with the SAME time used between the knots
-    """
-
-    def __init__(self, args: Namespace, robot: SingleArmInterface, x0: np.ndarray):
-        goal = robot.T_w_e
-        super().__init__(args, robot, x0, goal)
-
-    def updateCosts(self, data):
-        for i, runningModel in enumerate(self.solver.problem.runningModels):
-            runningModel.differential.costs.costs[
-                "ee_pose" + str(i)
-            ].cost.residual.reference = data[i]
-
-        self.solver.problem.terminalModel.differential.costs.costs[
-            "ee_pose" + str(self.args.n_knots)
-        ].cost.residual.reference = data[-1]
-
-
-class BaseAndEEPathFollowingOCP(BaseAndSingleArmIKOCP):
-    """
-    createBaseAndEEPathFollowingOCP
-    -------------------------------
-    creates a path following problem.
-    it is instantiated to just to stay at the current position.
-    NOTE: the path MUST be time indexed with the SAME time used between the knots
-    """
-
-    def __init__(self, args, robot: SingleArmWholeBodyInterface, x0):
-        goal = (robot.T_w_e, robot.T_w_b.translation.copy())
-        super().__init__(args, robot, x0, goal)
-
-    def updateCosts(self, data):
-        path_base = data[0]
-        pathSE3 = data[1]
-        for i, runningModel in enumerate(self.solver.problem.runningModels):
-            runningModel.differential.costs.costs[
-                "base_translation" + str(i)
-            ].cost.residual.reference = path_base[i]
-            runningModel.differential.costs.costs[
-                "ee_pose" + str(i)
-            ].cost.residual.reference = pathSE3[i]
-
-        # idk if that's necessary
-        self.solver.problem.terminalModel.differential.costs.costs[
-            "base_translation" + str(self.args.n_knots)
-        ].cost.residual.reference = path_base[-1]
-        self.solver.problem.terminalModel.differential.costs.costs[
-            "ee_pose" + str(self.args.n_knots)
-        ].cost.residual.reference = pathSE3[-1]
-
-
-class DualArmEEPathFollowingOCP(DualArmIKOCP):
-    def __init__(self, args, robot: DualArmInterface, x0):
-        goal = (robot.T_w_l, robot.T_w_r)
-        super().__init__(args, robot, x0, goal)
-
-    def updateCosts(self, data):
-        pathSE3_l = data[0]
-        pathSE3_r = data[1]
-        for i, runningModel in enumerate(self.solver.problem.runningModels):
-            runningModel.differential.costs.costs[
-                "l_ee_pose" + str(i)
-            ].cost.residual.reference = pathSE3_l[i]
-            runningModel.differential.costs.costs[
-                "r_ee_pose" + str(i)
-            ].cost.residual.reference = pathSE3_r[i]
-
-        self.solver.problem.terminalModel.differential.costs.costs[
-            "l_ee_pose" + str(self.args.n_knots)
-        ].cost.residual.reference = pathSE3_l[-1]
-        self.solver.problem.terminalModel.differential.costs.costs[
-            "r_ee_pose" + str(self.args.n_knots)
-        ].cost.residual.reference = pathSE3_r[-1]
-
-
-class BaseAndDualArmEEPathFollowingOCP(BaseAndDualArmIKOCP):
-    def __init__(self, args, robot: DualArmWholeBodyInterface, x0):
-        goal = robot.T_w_l, robot.T_w_r, robot.T_w_b.translation
-        super().__init__(args, robot, x0, goal)
-
-    def updateCosts(self, data):
-        path_base = data[0]
-        pathSE3_l = data[1]
-        pathSE3_r = data[2]
-        for i, runningModel in enumerate(self.solver.problem.runningModels):
-            runningModel.differential.costs.costs[
-                "base_translation" + str(i)
-            ].cost.residual.reference = path_base[i]
-            runningModel.differential.costs.costs[
-                "l_ee_pose" + str(i)
-            ].cost.residual.reference = pathSE3_l[i]
-            runningModel.differential.costs.costs[
-                "r_ee_pose" + str(i)
-            ].cost.residual.reference = pathSE3_r[i]
-
-        # idk if that's necessary
-        self.solver.problem.terminalModel.differential.costs.costs[
-            "base_translation" + str(self.args.n_knots)
-        ].cost.residual.reference = path_base[-1]
-        self.solver.problem.terminalModel.differential.costs.costs[
-            "l_ee_pose" + str(self.args.n_knots)
-        ].cost.residual.reference = pathSE3_l[-1]
-        self.solver.problem.terminalModel.differential.costs.costs[
-            "r_ee_pose" + str(self.args.n_knots)
-        ].cost.residual.reference = pathSE3_r[-1]
diff --git a/python/smc/control/optimal_control/point_to_point_croco_ocp.py b/python/smc/control/optimal_control/point_to_point_croco_ocp.py
deleted file mode 100644
index 1e6fada56b7244c272a1a5190943c7a5d3694d7f..0000000000000000000000000000000000000000
--- a/python/smc/control/optimal_control/point_to_point_croco_ocp.py
+++ /dev/null
@@ -1,313 +0,0 @@
-from smc.control.optimal_control.abstract_croco_ocp import CrocoOCP
-from smc.robots.interfaces.single_arm_interface import SingleArmInterface
-from smc.robots.interfaces.dual_arm_interface import DualArmInterface
-from smc.robots.interfaces.whole_body_interface import (
-    SingleArmWholeBodyInterface,
-    DualArmWholeBodyInterface,
-)
-
-import numpy as np
-import pinocchio as pin
-import crocoddyl
-from argparse import Namespace
-
-
-class BaseIKOCP(CrocoOCP):
-    def __init__(
-        self,
-        args: Namespace,
-        robot: SingleArmInterface,
-        x0: np.ndarray,
-        p_basegoal: pin.SE3,
-    ):
-        super().__init__(args, robot, x0, p_basegoal)
-
-    def constructTaskCostsValues(self):
-        self.base_translation_cost_values = np.linspace(
-            self.args.base_translation_cost,
-            self.args.base_translation_cost
-            * self.args.linearly_increasing_task_cost_factor,
-            self.args.n_knots + 1,
-        )
-
-    def constructTaskObjectiveFunction(self, goal) -> None:
-        p_basegoal = goal
-        for i in range(self.args.n_knots):
-            baseTranslationResidual = crocoddyl.ResidualModelFrameTranslation(
-                self.state, self.robot.base_frame_id, p_basegoal, self.state.nv
-            )
-            baseTrackingCost = crocoddyl.CostModelResidual(
-                self.state, baseTranslationResidual
-            )
-            self.runningCostModels[i].addCost(
-                "base_translation" + str(i),
-                baseTrackingCost,
-                self.base_translation_cost_values[i],
-            )
-        self.terminalCostModel.addCost(
-            "base_translation" + str(self.args.n_knots),
-            baseTrackingCost,
-            self.base_translation_cost_values[-1],
-        )
-
-    # there is nothing to update in a point-to-point task
-    def updateCosts(self, data):
-        pass
-
-    def updateGoalInModels(self, goal) -> None:
-        p_basegoal = goal
-        for i, runningModel in enumerate(self.solver.problem.runningModels):
-            runningModel.differential.costs.costs[
-                "base_translation" + str(i)
-            ].cost.residual.reference = p_basegoal
-        self.solver.problem.terminalModel.differential.costs.costs[
-            "base_translation" + str(self.args.n_knots)
-        ].cost.residual.reference = p_basegoal
-
-
-class SingleArmIKOCP(CrocoOCP):
-    def __init__(
-        self,
-        args: Namespace,
-        robot: SingleArmInterface,
-        x0: np.ndarray,
-        T_w_goal: pin.SE3,
-    ):
-        super().__init__(args, robot, x0, T_w_goal)
-
-    def constructTaskCostsValues(self):
-        self.ee_pose_cost_values = np.linspace(
-            self.args.ee_pose_cost,
-            self.args.ee_pose_cost * self.args.linearly_increasing_task_cost_factor,
-            self.args.n_knots + 1,
-        )
-
-    def constructTaskObjectiveFunction(self, goal) -> None:
-        T_w_goal = goal
-        framePlacementResidual = crocoddyl.ResidualModelFramePlacement(
-            self.state,
-            self.robot.ee_frame_id,
-            T_w_goal.copy(),
-            self.state.nv,
-        )
-        goalTrackingCost = crocoddyl.CostModelResidual(
-            self.state, framePlacementResidual
-        )
-        # TODO: final velocity costs only make sense if you're running a single ocp, but not mpc!!
-        # TODO: have an argument or something to include or not include them!
-        # frameVelocityResidual = crocoddyl.ResidualModelFrameVelocity(
-        #    self.state,
-        #    self.robot.ee_frame_id,
-        #    pin.Motion(np.zeros(6)),
-        #    pin.ReferenceFrame.WORLD,
-        # )
-        # frameVelocityCost = crocoddyl.CostModelResidual(
-        #    self.state, frameVelocityResidual
-        # )
-        for i in range(self.args.n_knots):
-            self.runningCostModels[i].addCost(
-                "ee_pose" + str(i), goalTrackingCost, self.ee_pose_cost_values[i]
-            )
-        self.terminalCostModel.addCost(
-            "ee_pose" + str(self.args.n_knots),
-            goalTrackingCost,
-            self.ee_pose_cost_values[-1],
-        )
-        # self.terminalCostModel.addCost("velFinal", frameVelocityCost, 1e3)
-
-    # there is nothing to update in a point-to-point task
-    def updateCosts(self, data):
-        pass
-
-    def updateGoalInModels(self, goal) -> None:
-        T_w_goal = goal
-        for i, runningModel in enumerate(self.solver.problem.runningModels):
-            runningModel.differential.costs.costs[
-                "ee_pose" + str(i)
-            ].cost.residual.reference = T_w_goal
-        self.solver.problem.terminalModel.differential.costs.costs[
-            "ee_pose" + str(self.args.n_knots)
-        ].cost.residual.reference = T_w_goal
-
-
-class DualArmIKOCP(CrocoOCP):
-    def __init__(
-        self, args: Namespace, robot: DualArmInterface, x0: np.ndarray, goal: pin.SE3
-    ):
-        super().__init__(args, robot, x0, goal)
-
-    def constructTaskCostsValues(self):
-        self.ee_pose_cost_values = np.linspace(
-            self.args.ee_pose_cost,
-            self.args.ee_pose_cost * self.args.linearly_increasing_task_cost_factor,
-            self.args.n_knots + 1,
-        )
-
-    def constructTaskObjectiveFunction(self, goal) -> None:
-        T_w_lgoal, T_w_rgoal = goal
-        framePlacementResidual_l = crocoddyl.ResidualModelFramePlacement(
-            self.state,
-            self.robot.l_ee_frame_id,
-            T_w_lgoal.copy(),
-            self.state.nv,
-        )
-        framePlacementResidual_r = crocoddyl.ResidualModelFramePlacement(
-            self.state,
-            self.robot.r_ee_frame_id,
-            T_w_rgoal.copy(),
-            self.state.nv,
-        )
-        goalTrackingCost_l = crocoddyl.CostModelResidual(
-            self.state, framePlacementResidual_l
-        )
-        goalTrackingCost_r = crocoddyl.CostModelResidual(
-            self.state, framePlacementResidual_r
-        )
-        # TODO: final velocity costs only make sense if you're running a single ocp, but not mpc!!
-        # TODO: have an argument or something to include or not include them!
-
-        # frameVelocityCost_l = crocoddyl.CostModelResidual(
-        #    self.state, frameVelocityResidual_l
-        # frameVelocityResidual_l = crocoddyl.ResidualModelFrameVelocity(
-        #    self.state,
-        #    self.robot.l_ee_frame_id,
-        #    pin.Motion(np.zeros(6)),
-        #    pin.ReferenceFrame.WORLD,
-        # )
-        # frameVelocityResidual_r = crocoddyl.ResidualModelFrameVelocity(
-        #    self.state,
-        #    self.robot.r_ee_frame_id,
-        #    pin.Motion(np.zeros(6)),
-        #    pin.ReferenceFrame.WORLD,
-        # )
-        # )
-        # frameVelocityCost_r = crocoddyl.CostModelResidual(
-        #    self.state, frameVelocityResidual_r
-        # )
-        for i in range(self.args.n_knots):
-            self.runningCostModels[i].addCost(
-                "l_ee_pose" + str(i),
-                goalTrackingCost_l,
-                self.ee_pose_cost_values[i],
-            )
-            self.runningCostModels[i].addCost(
-                "r_ee_pose" + str(i),
-                goalTrackingCost_r,
-                self.ee_pose_cost_values[i],
-            )
-        self.terminalCostModel.addCost(
-            "l_ee_pose" + str(self.args.n_knots),
-            goalTrackingCost_l,
-            self.ee_pose_cost_values[-1],
-        )
-        self.terminalCostModel.addCost(
-            "r_ee_pose" + str(self.args.n_knots),
-            goalTrackingCost_r,
-            self.ee_pose_cost_values[-1],
-        )
-        # self.terminalCostModel.addCost("velFinal_l", frameVelocityCost_l, 1e3)
-        # self.terminalCostModel.addCost("velFinal_r", frameVelocityCost_r, 1e3)
-
-    # there is nothing to update in a point-to-point task
-    def updateCosts(self, data):
-        pass
-
-    def updateGoalInModels(self, goal) -> None:
-        T_w_lgoal, T_w_rgoal = goal
-        for i, runningModel in enumerate(self.solver.problem.runningModels):
-            runningModel.differential.costs.costs[
-                "l_ee_pose" + str(i)
-            ].cost.residual.reference = T_w_lgoal
-            runningModel.differential.costs.costs[
-                "r_ee_pose" + str(i)
-            ].cost.residual.reference = T_w_rgoal
-
-        self.solver.problem.terminalModel.differential.costs.costs[
-            "l_ee_pose" + str(self.args.n_knots)
-        ].cost.residual.reference = T_w_lgoal
-        self.solver.problem.terminalModel.differential.costs.costs[
-            "r_ee_pose" + str(self.args.n_knots)
-        ].cost.residual.reference = T_w_rgoal
-
-
-class BaseAndSingleArmIKOCP(SingleArmIKOCP, BaseIKOCP):
-    def __init__(
-        self,
-        args: Namespace,
-        robot: SingleArmWholeBodyInterface,
-        x0: np.ndarray,
-        goal,
-    ):
-        super().__init__(args, robot, x0, goal)
-
-    def constructTaskCostsValues(self):
-        self.base_translation_cost_values = np.linspace(
-            self.args.base_translation_cost,
-            self.args.base_translation_cost
-            * self.args.linearly_increasing_task_cost_factor,
-            self.args.n_knots + 1,
-        )
-        self.ee_pose_cost_values = np.linspace(
-            self.args.ee_pose_cost,
-            self.args.ee_pose_cost * self.args.linearly_increasing_task_cost_factor,
-            self.args.n_knots + 1,
-        )
-
-    def constructTaskObjectiveFunction(
-        self,
-        goal,
-    ) -> None:
-        T_w_eegoal, p_basegoal = goal
-        super().constructTaskObjectiveFunction(T_w_eegoal)
-        BaseIKOCP.constructTaskObjectiveFunction(self, p_basegoal)
-
-    # there is nothing to update in a point-to-point task
-    def updateCosts(self, data):
-        pass
-
-    def updateGoalInModels(self, goal) -> None:
-        # self, T_w_eegoal: pin.SE3, p_basegoal: np.ndarray) -> None:
-        T_w_eegoal, p_basegoal = goal
-        super().updateGoalInModels(T_w_eegoal)
-        BaseIKOCP.updateGoalInModels(self, p_basegoal)
-
-
-class BaseAndDualArmIKOCP(DualArmIKOCP, BaseIKOCP):
-    def __init__(
-        self,
-        args: Namespace,
-        robot: DualArmWholeBodyInterface,
-        x0: np.ndarray,
-        goal,
-    ):
-        super().__init__(args, robot, x0, goal)
-
-    def constructTaskCostsValues(self):
-        self.base_translation_cost_values = np.linspace(
-            self.args.base_translation_cost,
-            self.args.base_translation_cost
-            * self.args.linearly_increasing_task_cost_factor,
-            self.args.n_knots + 1,
-        )
-        self.ee_pose_cost_values = np.linspace(
-            self.args.ee_pose_cost,
-            self.args.ee_pose_cost * self.args.linearly_increasing_task_cost_factor,
-            self.args.n_knots + 1,
-        )
-
-    def constructTaskObjectiveFunction(
-        self,
-        goal,
-    ) -> None:
-        T_w_lgoal, T_w_rgoal, p_basegoal = goal
-        super().constructTaskObjectiveFunction((T_w_lgoal, T_w_rgoal))
-        BaseIKOCP.constructTaskObjectiveFunction(self, p_basegoal)
-
-    # there is nothing to update in a point-to-point task
-    def updateCosts(self, data) -> None:
-        pass
-
-    def updateGoalInModels(self, goal) -> None:
-        T_w_lgoal, T_w_rgoal, p_basegoal = goal
-        super().updateGoalInModels((T_w_lgoal, T_w_rgoal))
-        BaseIKOCP.updateGoalInModels(self, p_basegoal)
diff --git a/python/smc/path_generation/path_math/path_to_trajectory.py b/python/smc/path_generation/path_math/path_to_trajectory.py
index d92d3065c9d37a87c47a3081cefe3925450f3803..08ef8f9a3498ecbfa74a310ad5e556e6114c70a9 100644
--- a/python/smc/path_generation/path_math/path_to_trajectory.py
+++ b/python/smc/path_generation/path_math/path_to_trajectory.py
@@ -1,5 +1,6 @@
 import numpy as np
 from argparse import Namespace
+from pinocchio import SE3
 
 
 def computePath2DLength(path2D):
@@ -41,3 +42,39 @@ def path2D_to_trajectory2D(
         ]
     ).T
     return trajectory2D
+
+
+def pathSE3_to_trajectorySE3(
+    args: Namespace, pathSE3: np.ndarray, velocity: float
+) -> list[SE3]:
+    path2D = np.zeros((len(pathSE3), 2))
+    for i, pose in enumerate(pathSE3):
+        path2D[i] = pose.translation[:2]
+    path_length = computePath2DLength(path2D)
+    # NOTE: sometimes the path planner gives me the same god damn points
+    # and that's it. can't do much about, expect set those point then.
+    # and i need to return now so that the math doesn't break down the line
+    if path_length < 1e-3:
+        return [pathSE3[0]] * (args.n_knots + 1)
+    total_time = path_length / velocity
+    # NOTE: assuming the path is uniformly sampled
+    t_path = np.linspace(0.0, total_time, len(path2D))
+    t_ocp = np.linspace(0.0, args.ocp_dt * (args.n_knots + 1), args.n_knots + 1)
+
+    trajectorySE3 = []
+    path_index = 0
+    for t_traj in t_ocp:
+        if t_traj > t_path[path_index + 1]:
+            if path_index < len(pathSE3) - 2:
+                path_index += 1
+
+        if path_index < len(pathSE3) - 2:
+            pose_traj = SE3.Interpolate(
+                pathSE3[path_index],
+                pathSE3[path_index + 1],
+                t_traj - t_path[path_index],
+            )
+            trajectorySE3.append(pose_traj)
+        else:
+            trajectorySE3.append(pathSE3[-1])
+    return trajectorySE3
diff --git a/python/smc/robots/abstract_robotmanager.py b/python/smc/robots/abstract_robotmanager.py
index b8fff65a0ab2b3941a1909c75f05a1ac9a02ff93..baa81d88320cea0f6982a8f0e25c68dff3cb8507 100644
--- a/python/smc/robots/abstract_robotmanager.py
+++ b/python/smc/robots/abstract_robotmanager.py
@@ -77,9 +77,9 @@ class AbstractRobotManager(abc.ABC):
         self._max_v = np.clip(self._MAX_V, 0.0, args.max_v_percentage * self._MAX_V)
         # NOTE: make sure you've read pinocchio docs and understand why
         #       nq and nv are not necessarily the same number
-        self._q = np.zeros(self.nq)
-        self._v = np.zeros(self.nv)
-        self._a = np.zeros(self.nv)
+        self._q = np.zeros(self.model.nq)
+        self._v = np.zeros(self.model.nv)
+        self._a = np.zeros(self.model.nv)
 
         self._comfy_configuration: np.ndarray
 
@@ -331,6 +331,11 @@ class AbstractRobotManager(abc.ABC):
         else:
             print("not implemented yet, so nothing is going to happen!")
 
+
+    def getJacobian(self) -> np.ndarray:
+        ...
+
+
     ########################################################################################
     # visualizer management. ideally transferred elsewhere
     ###################################################################################
diff --git a/python/smc/robots/implementations/mir.py b/python/smc/robots/implementations/mir.py
index f64f9e20c39341ed9691600beb05926be54eb781..33bb99e3ac092ae2561ddb271bed42806706ab75 100644
--- a/python/smc/robots/implementations/mir.py
+++ b/python/smc/robots/implementations/mir.py
@@ -26,6 +26,14 @@ class SimulatedMirRobotManager(AbstractSimulatedRobotManager, AbstractMirRobotMa
             print("SimulatedMirRobotManager init")
         super().__init__(args)
 
+    def setInitialPose(self):
+        self._q = np.zeros(4)
+        self._q[0] = np.random.random()
+        self._q[1] = np.random.random()
+        theta = np.random.random() * 2 * np.pi - np.pi
+        self._q[2] = np.cos(theta)
+        self._q[3] = np.sin(theta)
+
 
 class RealMirRobotManager(AbstractMirRobotManager):
     # TODO: implement
diff --git a/python/smc/robots/implementations/mobile_yumi.py b/python/smc/robots/implementations/mobile_yumi.py
index 28af048a563e96f679d7988996ef86faf8b46869..1e76a5c00d73fa81a9a0bb6a088aadd358b3b183 100644
--- a/python/smc/robots/implementations/mobile_yumi.py
+++ b/python/smc/robots/implementations/mobile_yumi.py
@@ -30,6 +30,28 @@ class AbstractMobileYuMiRobotManager(DualArmWholeBodyInterface):
         self._mode: DualArmWholeBodyInterface.control_mode = (
             DualArmWholeBodyInterface.control_mode.whole_body
         )
+        self._comfy_configuration = np.array(
+            [
+                0.0, # x 
+                0.0, # y
+                1.0, # cos(theta)
+                0.0, # sin(theta)
+                0.045,
+                -0.155,
+                -0.394,
+                -0.617,
+                -0.939,
+                -0.343,
+                -1.216,
+                -0.374,
+                -0.249,
+                0.562,
+                -0.520,
+                0.934,
+                -0.337,
+                1.400,
+            ]
+        )
         super().__init__(args)
 
 
diff --git a/python/smc/robots/interfaces/dual_arm_interface.py b/python/smc/robots/interfaces/dual_arm_interface.py
index a19fbd05f18781300446e2b72ba789ea0d512f66..e6bf2f06a49802347c9ce88b22ebfa3674043471 100644
--- a/python/smc/robots/interfaces/dual_arm_interface.py
+++ b/python/smc/robots/interfaces/dual_arm_interface.py
@@ -232,3 +232,50 @@ class DualArmInterface(SingleArmInterface):
 
         v_cmd_to_real = np.clip(v_cmd_to_real, -1 * self._max_v, self._max_v)
         self.sendVelocityCommandToReal(v_cmd_to_real)
+
+    # NOTE: we almost certainly want to compute this w.r.t. T_w_abs
+    # but i don't have time to write all the math out
+    def computeManipulabilityIndexQDerivative(self) -> np.ndarray:
+
+        def oneArm(joint_id: int) -> np.ndarray:
+            J = pin.computeJointJacobian(self.model, self.data, self._q, joint_id)
+            Jp = J.T @ np.linalg.inv(
+                J @ J.T + np.eye(J.shape[0], J.shape[0]) * self.args.tikhonov_damp
+            )
+            # res = np.zeros(self.nv)
+            # v0 = np.zeros(self.nv)
+            res = np.zeros(self.model.nv)
+            v0 = np.zeros(self.model.nv)
+            for k in range(6):
+                pin.computeForwardKinematicsDerivatives(
+                    self.model,
+                    self.data,
+                    self._q,
+                    Jp[:, k],
+                    v0,
+                    # self.model,
+                    # self.data,
+                    # self._q,
+                    # v0,
+                    # np.zeros(self.model.nv),
+                )
+                JqJpk = pin.getJointVelocityDerivatives(
+                    self.model, self.data, joint_id, pin.LOCAL
+                )[0]
+                res += JqJpk[k, :]
+            res *= self.computeManipulabilityIndex()
+            return res
+
+        l_joint_index = self.model.frames[self._l_ee_frame_id].parent
+        r_joint_index = self.model.frames[self._r_ee_frame_id].parent
+
+        # TODO: joint_ids obviously have to be defined per robot, this is a dirty hack
+        # because i know i'm on yumi
+        res_left = oneArm(l_joint_index)
+        if self._mode == AbstractRobotManager.control_mode.left_arm_only:
+            return res_left[:l_joint_index]
+        res_right = oneArm(r_joint_index)
+        if self._mode == AbstractRobotManager.control_mode.right_arm_only:
+            return res_right[l_joint_index:]
+
+        return res_left + res_right
diff --git a/python/smc/robots/interfaces/single_arm_interface.py b/python/smc/robots/interfaces/single_arm_interface.py
index 31f00455a73f723cabaf3d455f751b495c5a87de..fbe85ee2aa44674d8c58a44ed07c0f7fa5741a3b 100644
--- a/python/smc/robots/interfaces/single_arm_interface.py
+++ b/python/smc/robots/interfaces/single_arm_interface.py
@@ -56,3 +56,41 @@ class SingleArmInterface(AbstractRobotManager):
         self._updateQ()
         self._updateV()
         self.forwardKinematics()
+
+    # NOTE: manipulability calculations are here
+    # only because i have no idea where to put them at the moment
+    # TODO: put them in a better place
+
+    def computeManipulabilityIndex(self) -> np.ndarray:
+        J = self.getJacobian()
+        return np.sqrt(np.linalg.det(J @ J.T))
+
+    def computeManipulabilityIndexQDerivative(self) -> np.ndarray:
+        joint_index = self.model.frames[self._ee_frame_id].parent
+        J = pin.computeJointJacobian(self.model, self.data, self._q, joint_index)
+        Jp = J.T @ np.linalg.inv(
+            J @ J.T + np.eye(J.shape[0], J.shape[0]) * self.args.tikhonov_damp
+        )
+        # res = np.zeros(self.nv)
+        # v0 = np.zeros(self.nv)
+        res = np.zeros(self.model.nv)
+        v0 = np.zeros(self.model.nv)
+        for k in range(6):
+            pin.computeForwardKinematicsDerivatives(
+                self.model,
+                self.data,
+                self._q,
+                Jp[:, k],
+                v0,
+                # self.model,
+                # self.data,
+                # self._q,
+                # v0,
+                # np.zeros(self.model.nv),
+            )
+            JqJpk = pin.getJointVelocityDerivatives(
+                self.model, self.data, joint_index, pin.LOCAL
+            )[0]
+            res += JqJpk[k, :]
+        res *= self.computeManipulabilityIndex()
+        return res
diff --git a/python/smc/util/draw_path.py b/python/smc/util/draw_path.py
index 3b3a7f29f4282a470a97a0c4f4c7514958bc081f..a32b614a5df2c1255d5038b4f9341192424365d1 100644
--- a/python/smc/util/draw_path.py
+++ b/python/smc/util/draw_path.py
@@ -52,12 +52,14 @@ class DrawPathManager:
                 print("pixel path:")
                 print(self.path)
             self.disconnect()
-            np.savetxt("./path_in_pixels.csv", self.path, delimiter=",", fmt="%.5f")
+            np.savetxt(
+                "./parameters/path_in_pixels.csv", self.path, delimiter=",", fmt="%.5f"
+            )
             # plt.close over exit so that we can call this from elsewhere and not kill the program
             plt.close()
 
 
-def drawPath(args:Namespace)->np.ndarray:
+def drawPath(args: Namespace) -> np.ndarray:
     # normalize both x and y to 0-1 range
     # we can multiply however we want later
     # idk about the number of points, but it's large enough to draw