diff --git a/python/examples/crocoddyl_ocp_clik.py b/python/examples/crocoddyl_ocp_clik.py
index 84d2a1cea22016e8810f8796c449ace62f6f2b8e..4763547af329a0c642f3441909079a728d079934 100644
--- a/python/examples/crocoddyl_ocp_clik.py
+++ b/python/examples/crocoddyl_ocp_clik.py
@@ -4,9 +4,11 @@ import argparse
 from functools import partial
 from ur_simple_control.managers import getMinimalArgParser, ControlLoopManager, RobotManager
 from ur_simple_control.optimal_control.crocoddyl_optimal_control import get_OCP_args, CrocoIKOCP
-from ur_simple_control.basics.basics import jointTrajFollowingPD
+from ur_simple_control.basics.basics import followKinematicJointTrajP
 from ur_simple_control.util.logging_utils import LogManager
+from ur_simple_control.visualize.visualize import plotFromDict
 import pinocchio as pin
+import crocoddyl
 
 
 def get_args():
@@ -19,22 +21,26 @@ def get_args():
 if __name__ == "__main__": 
     args = get_args()
     robot = RobotManager(args)
-    Mgoal = robot.defineGoalPointCLI()
+    # TODO: put this back for nicer demos
+    #Mgoal = robot.defineGoalPointCLI()
+    Mgoal = pin.SE3.Random()
     # create and solve the optimal control problem of
     # getting from current to goal end-effector position.
     # reference is position and velocity reference (as a dictionary),
     # while solver is a crocoddyl object containing a lot more information
     reference, solver = CrocoIKOCP(args, robot, Mgoal)
+    log = solver.getCallbacks()[1]
+    crocoddyl.plotOCSolution(log.xs, log.us, figIndex=1, show=True)
     # we need a way to follow the reference trajectory,
     # both because there can be disturbances,
     # and because it is sampled at a much lower frequency
-    jointTrajFollowingPD(args, robot, reference)
-    #reference.pop('dt')
-    #plotFromDict(reference, args.n_knots + 1, args)
+    followKinematicJointTrajP(args, robot, reference)
 
     print("final position:")
     print(robot.getT_w_e())
 
+    robot.log_manager.plotAllControlLoops()
+
     if not args.pinocchio_only:
         robot.stopRobot()
 
diff --git a/python/examples/data/latest_run b/python/examples/data/latest_run
new file mode 100644
index 0000000000000000000000000000000000000000..107c2246481edf0fd0cf7df9ee13b5c4eed62b68
Binary files /dev/null and b/python/examples/data/latest_run differ
diff --git a/python/examples/heron_pls.py b/python/examples/heron_pls.py
index 346ff92fe46f84f3190c2710a0860472e3920ad9..5eb9f9153e58c6c05afc8bd116abcd3ef36619df 100644
--- a/python/examples/heron_pls.py
+++ b/python/examples/heron_pls.py
@@ -11,14 +11,13 @@ import pinocchio as pin
 def get_args():
     parser = getMinimalArgParser()
     parser.description = 'mpc for heron path following'
-    parser = get_OCP_args(parser)
     args = parser.parse_args()
     return args
 
 if __name__ == "__main__": 
     args = get_args()
 
-    model, collision_model, visual_model, data = heron_approximation(args)
+    model, collision_model, visual_model, data = heron_approximation()
     print(model)
     viz = MeshcatVisualizer(model, collision_model, visual_model)
     viz.initViewer(open=True)
diff --git a/python/ur_simple_control/__pycache__/managers.cpython-312.pyc b/python/ur_simple_control/__pycache__/managers.cpython-312.pyc
index eebd716670a2552ef6b2850384768b390ae8cf3d..88c6cb821ec8dc97c3b71076271c9edadd6dadf8 100644
Binary files a/python/ur_simple_control/__pycache__/managers.cpython-312.pyc and b/python/ur_simple_control/__pycache__/managers.cpython-312.pyc differ
diff --git a/python/ur_simple_control/basics/__pycache__/basics.cpython-312.pyc b/python/ur_simple_control/basics/__pycache__/basics.cpython-312.pyc
index 1faaaed581604ec4087a48d6b79a04fa2e648a92..0e87b163ebbfe0936a10a8fdbb724fb20f9fdd74 100644
Binary files a/python/ur_simple_control/basics/__pycache__/basics.cpython-312.pyc and b/python/ur_simple_control/basics/__pycache__/basics.cpython-312.pyc differ
diff --git a/python/ur_simple_control/basics/basics.py b/python/ur_simple_control/basics/basics.py
index c8f48d08b458601ee23f36b92d2cfcec641dd81a..ed1d53be8dc25f800093d5ad2d4c3e92c12e98c2 100644
--- a/python/ur_simple_control/basics/basics.py
+++ b/python/ur_simple_control/basics/basics.py
@@ -72,7 +72,7 @@ def moveJP(args, robot, q_desired):
 # also NOTE: there is an argument to be made for pre-interpolating the reference.
 # this is because joint positions will be more accurate.
 # if we integrate them with interpolated velocities.
-def jointTrajFollowingPDControlLoop(stop_at_final : bool, robot: RobotManager, reference, i, past_data):
+def followKinematicJointTrajPControlLoop(stop_at_final : bool, robot: RobotManager, reference, i, past_data):
     breakFlag = False
     save_past_dict = {}
     log_item = {}
@@ -82,11 +82,10 @@ def jointTrajFollowingPDControlLoop(stop_at_final : bool, robot: RobotManager, r
     # which is pretty much true
     t = i * robot.dt
     # take the future (next) one
-    # TODO: turn this into interpolation
     t_index_lower = int(np.floor(t / reference['dt']))
     t_index_upper = int(np.ceil(t / reference['dt']))
 
-    # TODO: move to a different function later
+    # TODO: set q_refs and v_refs once (merge with interpolation if)
     if t_index_upper >= len(reference['qs']) - 1:
         t_index_upper = len(reference['qs']) - 1
     q_ref = reference['qs'][t_index_upper]
@@ -95,17 +94,13 @@ def jointTrajFollowingPDControlLoop(stop_at_final : bool, robot: RobotManager, r
     else:
         v_ref = reference['vs'][t_index_upper]
     
-    # TODO: check if that's really the last
-    #if t_index_upper == len(reference['qs']) - 1:
-    #    breakFlag = True
-
     # TODO NOTE TODO TODO: move under stop/don't stop at final argument
     if (t_index_upper == len(reference['qs']) - 1) and \
             (np.linalg.norm(q - reference['qs'][-1]) < 1e-2) and \
             (np.linalg.norm(v) < 1e-2):
         breakFlag = True
 
-    # TODO: set q_refs and v_refs once
+    # TODO: move interpolation to a different function later
     if (t_index_upper < len(reference['qs']) - 1) and (not breakFlag):
         #angle = (reference['qs'][t_index_upper] - reference['qs'][t_index_lower]) / reference['dt']
         angle_v = (reference['vs'][t_index_upper] - reference['vs'][t_index_lower]) / reference['dt']
@@ -118,8 +113,6 @@ def jointTrajFollowingPDControlLoop(stop_at_final : bool, robot: RobotManager, r
         q_ref = pin.integrate(robot.model, reference['qs'][t_index_lower], reference['vs'][t_index_lower] * time_difference)
 
 
-
-
     if robot.robot_name == "ur5e":
         error_q = q_ref - q
     if robot.robot_name == "heron":
@@ -143,9 +136,9 @@ def jointTrajFollowingPDControlLoop(stop_at_final : bool, robot: RobotManager, r
 
     return breakFlag, {}, log_item
 
-def jointTrajFollowingPD(args, robot, reference):
+def followKinematicJointTrajP(args, robot, reference):
     # we're not using any past data or logging, hence the empty arguments
-    controlLoop = partial(jointTrajFollowingPDControlLoop, args.stop_at_final, robot, reference)
+    controlLoop = partial(followKinematicJointTrajPControlLoop, args.stop_at_final, robot, reference)
     log_item = {
         'error_qs' : np.zeros(robot.model.nq),
         'error_vs' : np.zeros(robot.model.nv),
diff --git a/python/ur_simple_control/managers.py b/python/ur_simple_control/managers.py
index a23fba31207a3e68da78bce780f81d1f34983734..f96d0e303ebf81b3593e469ad4ffc057cbcb84b9 100644
--- a/python/ur_simple_control/managers.py
+++ b/python/ur_simple_control/managers.py
@@ -79,6 +79,8 @@ def getMinimalArgParser():
                     default="192.168.1.102")
     parser.add_argument('--pinocchio-only', action=argparse.BooleanOptionalAction, \
             help="whether you want to just integrate with pinocchio", default=False)
+    parser.add_argument('--fast-simulation', action=argparse.BooleanOptionalAction, \
+            help="do you want simulation to run over 500Hz? (real-time viz relies on 500Hz)", default=False)
     parser.add_argument('--visualize-manipulator', action=argparse.BooleanOptionalAction, \
             help="whether you want to visualize the manipulator and workspace with meshcat", default=False)
     parser.add_argument('--real-time-plotting', action=argparse.BooleanOptionalAction, \
@@ -116,7 +118,7 @@ def getMinimalArgParser():
             help="name the whole run/experiment (name of log file). note that indexing of runs is automatic and under a different argument.", \
             default='latest_run')
     parser.add_argument('--index-runs', action=argparse.BooleanOptionalAction, \
-            help="if you want more runs of the same name, this option will automatically assign an index to a new run (useful for data collection).", default=True)
+            help="if you want more runs of the same name, this option will automatically assign an index to a new run (useful for data collection).", default=False)
     parser.add_argument('--past-window-size', type=int, \
             help="how many timesteps of past data you want to save", default=5)
     # maybe you want to scale the control signal (TODO: NOT HERE)
@@ -294,7 +296,12 @@ class ControlLoopManager:
                     print("missed deadline by", diff - self.robot_manager.dt)
                 continue
             else:
-                time.sleep(self.robot_manager.dt - diff)
+                # there's no point in sleeping if we're in simulation
+                # NOTE: it literally took me a year to put this if here
+                # and i have no idea why i didn't think of it before lmao 
+                # (because i did know about it, just didn't even think of changing it)
+                if (not self.args.pinocchio_only) and (not self.args.fast_simulation):
+                    time.sleep(self.robot_manager.dt - diff)
 
 
         if self.args.debug_prints:
@@ -494,6 +501,10 @@ class RobotManager:
         # also TODO: figure out how to best solve the gripper_velocity problem
         # NOTE: you need to initialize differently for other types of joints
         self.q = np.zeros(self.model.nq)
+        # planar joint is [x, y, cos(theta), sin(theta)],
+        # so it can't be all zeros
+        if self.robot_name == "heron":
+            self.q[2] = 1.0
         # v_q is the generalization of qd for every type of joint.
         # for revolute joints it's qd, but for ex. the planar joint it's the body velocity.
         self.v_q = np.zeros(self.model.nv)
diff --git a/python/ur_simple_control/optimal_control/crocoddyl_optimal_control.py b/python/ur_simple_control/optimal_control/crocoddyl_optimal_control.py
index 8faf38ad94f7a9e28f5c3a89ff729de346cd8c1d..1869c54184fc2e100a911079cd43ab5dd5dba30c 100644
--- a/python/ur_simple_control/optimal_control/crocoddyl_optimal_control.py
+++ b/python/ur_simple_control/optimal_control/crocoddyl_optimal_control.py
@@ -3,7 +3,7 @@ import pinocchio as pin
 import crocoddyl
 from ur_simple_control.managers import getMinimalArgParser, ControlLoopManager, RobotManager
 import argparse
-from ur_simple_control.basics.basics import jointTrajFollowingPD
+from ur_simple_control.basics.basics import followKinematicJointTrajP
 from ur_simple_control.visualize.visualize import plotFromDict
 import example_robot_data
 import time
@@ -27,7 +27,7 @@ def CrocoIKOCP(args, robot : RobotManager, goal : pin.SE3):
     #print(robot.model.effortLimit)
     #exit()
     #robot.model.effortLimit = robot.model.effortLimit * (args.acceleration / robot.MAX_ACCELERATION)
-    #robot.model.effortLimit = robot.model.effortLimit * 0.01
+    #robot.model.effortLimit = robot.model.effortLimit * 0.5
     #robot.data = robot.model.createData()
     # starting state
     x0 = np.concatenate([robot.getQ(), robot.getQd()])
@@ -82,32 +82,33 @@ def CrocoIKOCP(args, robot : RobotManager, goal : pin.SE3):
     xub = np.concatenate([
         robot.model.upperPositionLimit,
         robot.model.velocityLimit])
+    # we have no limits on the mobile base.
+    # the mobile base is a planar joint.
+    # since it is represented as [x,y,cos(theta),sin(theta)], there is no point
+    # to limiting cos(theta),sin(theta) even if we wanted limits,
+    # because we would then limit theta, or limit ct and st jointly.
+    # in any event, xlb and xub are 1 number too long --
+    # the residual has to be [x,y,theta] because it is in the tangent space - 
+    # the difference between two points on a manifold in pinocchio is defined
+    # as the velocity which if parallel transported for 1 unit of "time" 
+    # from one to point to the other.
+    # point activation input and the residual need to be of the same length obviously,
+    # and this should be 2 * model.nv the way things are defined here.
+    if robot.robot_name == "heron":
+        xlb = xlb[1:]
+        xub = xub[1:]
     bounds = crocoddyl.ActivationBounds(xlb, xub, 1.0)
-    xLimitResidual = crocoddyl.ResidualModelState(state, x0, actuation.nu)
+    xLimitResidual = crocoddyl.ResidualModelState(state, x0, state.nv)
     xLimitActivation = crocoddyl.ActivationModelQuadraticBarrier(bounds)
 
-    # TOOODOOO ADD THE SAME FOR WHOLE HERON
-    if robot.robot_name == "ur5e":
-        limitCost = crocoddyl.CostModelResidual(state, xLimitActivation, xLimitResidual)
-        runningCostModel.addCost("limitCost", limitCost, 1e3)
-        terminalCostModel.addCost("limitCost", limitCost, 1e3)
+    print(len(x0), len(xlb), len(xub))
+    print(xLimitResidual.nr)
+    print(xLimitActivation.nr)
+    limitCost = crocoddyl.CostModelResidual(state, xLimitActivation, xLimitResidual)
+    runningCostModel.addCost("limitCost", limitCost, 1e3)
+    terminalCostModel.addCost("limitCost", limitCost, 1e3)
 
 
-    # NOTE: i have no idea how to incorporate this into anything
-    # we need to add state constraints for things to make sense
-    # NOTE!!!: there are no hard inequality constraints on states
-    # in what's implemented in crocoddyl. 
-    # INSTEAD, you define quadratic barier-type costs for that.
-    # there are box constraints on control input though,
-    # but i'm pretty sure i'm not using those correctly.
-    #x_constraint_lower = x0.copy()
-    #x_constraint_upper = x0.copy()
-    #x_constraint_lower[:robot.model.nq] = -2 * np.pi
-    #x_constraint_upper[:robot.model.nq] = 2 * np.pi
-    #x_constraint_lower[robot.model.nq:] = -1 * robot.max_qd
-    #x_constraint_upper[robot.model.nq:] = robot.max_qd
-    #state_constraint = crocoddyl.ConstraintModelResidual(state, xResidual, x_constraint_lower, x_constraint_upper)
-
     # Next, we need to create an action model for running and terminal knots. The
     # forward dynamics (computed using ABA) are implemented
     # inside DifferentialActionModelFreeFwdDynamics.
@@ -129,7 +130,7 @@ def CrocoIKOCP(args, robot : RobotManager, goal : pin.SE3):
     # and the solver
     solver = crocoddyl.SolverBoxFDDP(problem)
     #solver = crocoddyl.SolverIpopt(problem)
-    # TODO: remove / place elsewhere once it works
+    # TODO: remove / place elsewhere once it works (make it an argument)
     solver.setCallbacks([crocoddyl.CallbackVerbose(), crocoddyl.CallbackLogger()])
     # run solve
     # NOTE: there are some possible parameters here that I'm not using
@@ -172,7 +173,7 @@ if __name__ == "__main__":
     log = solver.getCallbacks()[1]
     crocoddyl.plotOCSolution(log.xs, log.us, figIndex=1, show=True)
     crocoddyl.plotConvergence(log.costs, log.pregs, log.dregs, log.grads, log.stops, log.steps, figIndex=2)
-    jointTrajFollowingPD(args, robot, reference)
+    followKinematicJointTrajP(args, robot, reference)
     reference.pop('dt')
     plotFromDict(reference, args.n_knots + 1, args)
     print("achieved result", robot.getT_w_e())
diff --git a/python/ur_simple_control/util/__pycache__/get_model.cpython-312.pyc b/python/ur_simple_control/util/__pycache__/get_model.cpython-312.pyc
index 7fb311856ef9a0ccfa2de4ba05ba86e1b0356785..172fcd77d0c1c699029769b4d378712d1d374d19 100644
Binary files a/python/ur_simple_control/util/__pycache__/get_model.cpython-312.pyc and b/python/ur_simple_control/util/__pycache__/get_model.cpython-312.pyc differ
diff --git a/python/ur_simple_control/util/__pycache__/logging_utils.cpython-312.pyc b/python/ur_simple_control/util/__pycache__/logging_utils.cpython-312.pyc
index fe80f5e9d71c11753cadb1118869190383545e4c..f87a89b70ea42b66252ea0a118e7eb21c5ad2837 100644
Binary files a/python/ur_simple_control/util/__pycache__/logging_utils.cpython-312.pyc and b/python/ur_simple_control/util/__pycache__/logging_utils.cpython-312.pyc differ
diff --git a/python/ur_simple_control/util/get_model.py b/python/ur_simple_control/util/get_model.py
index a5737ee1084517f9f6c1a1550e1d6d8680df75c6..ce274d50aaf896af13d3306a6f6c636e338ede11 100644
--- a/python/ur_simple_control/util/get_model.py
+++ b/python/ur_simple_control/util/get_model.py
@@ -125,8 +125,26 @@ def heron_approximation():
     joint_placement = pin.SE3.Identity()
     #joint_placement.rotation = pin.rpy.rpyToMatrix(0, -np.pi/2, 0) 
     #joint_placement.translation[2] = 0.2
+    # TODO TODO TODO TODO TODO TODO TODO TODO
+    # TODO: heron is actually a differential drive,
+    # meaning that it is not a planar joint.
+    # we could put in a prismatic + revolute joint
+    # as the base (both joints being at same position),
+    # and that should work for our purposes.
+    # this makes sense for initial testing
+    # because mobile yumi's base is a planar joint
     MOBILE_BASE_JOINT_ID = model_mobile_base.addJoint(parent_id, pin.JointModelPlanar(), 
             joint_placement.copy(), joint_name)
+    # we should immediately set velocity limits.
+    # there are no position limit by default and that is what we want.
+    # TODO: put in heron's values
+    model_mobile_base.velocityLimit[0] = 2
+    model_mobile_base.velocityLimit[1] = 2
+    model_mobile_base.velocityLimit[2] = 2
+    # TODO: i have literally no idea what reasonable numbers are here
+    model_mobile_base.effortLimit[0] = 200
+    model_mobile_base.effortLimit[1] = 200
+    model_mobile_base.effortLimit[2] = 200
     #print("OBJECT_JOINT_ID",OBJECT_JOINT_ID)
     #body_inertia = pin.Inertia.FromBox(args.box_mass, box_dimensions[0], 
     #        box_dimensions[1], box_dimensions[2])
diff --git a/python/ur_simple_control/util/logging_utils.py b/python/ur_simple_control/util/logging_utils.py
index 291500fb6ddb8b265d68cd0bad2abf2c75c4a265..948637e31866e25fdedb6a586cd9be16ecf795c7 100644
--- a/python/ur_simple_control/util/logging_utils.py
+++ b/python/ur_simple_control/util/logging_utils.py
@@ -3,6 +3,8 @@ import numpy as np
 import os
 import subprocess
 import re
+from ur_simple_control.visualize.visualize import plotFromDict
+
 
 
 class LogManager:
@@ -21,6 +23,7 @@ class LogManager:
         if args is None:
             return
         self.args = args
+        self.are_logs_vectorized_flag = False
         self.loop_logs = {}
         self.loop_number = 0
         # name the run
@@ -52,6 +55,11 @@ class LogManager:
         self.loop_number += 1
         self.loop_logs[loop_name] = log_dict
 
+    def vectorizeLog(self):
+        for control_loop_name in self.loop_logs:
+            for key in self.loop_logs[control_loop_name]:
+                self.loop_logs[control_loop_name][key] = np.array(self.loop_logs[control_loop_name][key])
+        self.are_logs_vectorized_flag = True
 
     def saveLog(self, cleanUpRun=False):
         """
@@ -63,9 +71,8 @@ class LogManager:
         Uses default pickling.
         """
         # finally transfer to numpy (after nothing is running anymore)
-        for control_loop_name in self.loop_logs:
-            for key in self.loop_logs[control_loop_name]:
-                self.loop_logs[control_loop_name][key] = np.array(self.loop_logs[control_loop_name][key])
+        if not self.are_logs_vectorized_flag:
+            self.vectorizeLog()
         if cleanUpRun:
             self.cleanUpRun()
         print(f"data is ready, logmanager will now save your logs to \
@@ -88,6 +95,14 @@ class LogManager:
         self.__dict__.clear()
         self.__dict__.update(tmp_dict)
 
+    def plotAllControlLoops(self):
+        if not self.are_logs_vectorized_flag:
+            self.vectorizeLog() 
+            self.are_logs_vectorized_flag = True
+
+        for control_loop_name in self.loop_logs:
+            plotFromDict(self.loop_logs[control_loop_name], len(self.loop_logs[control_loop_name]['qs']), self.args)
+
 
     def findLatestIndex(self):
         """
diff --git a/python/ur_simple_control/visualize/__pycache__/visualize.cpython-312.pyc b/python/ur_simple_control/visualize/__pycache__/visualize.cpython-312.pyc
index 7039720b6b1d9858d08e7c3911c57c5324a85ff2..9dd45e1af3f798ed65e27a6b9dcc08e32431e013 100644
Binary files a/python/ur_simple_control/visualize/__pycache__/visualize.cpython-312.pyc and b/python/ur_simple_control/visualize/__pycache__/visualize.cpython-312.pyc differ
diff --git a/python/ur_simple_control/visualize/visualize.py b/python/ur_simple_control/visualize/visualize.py
index 0215e599f385bc8268641ecaa48baa462c7760d8..30221a7d14c635ff3f23ec225013d03145e9793b 100644
--- a/python/ur_simple_control/visualize/visualize.py
+++ b/python/ur_simple_control/visualize/visualize.py
@@ -34,14 +34,14 @@ def getNRowsMColumnsFromTotalNumber(n_plots):
     return n_rows, n_cols
 
 
-""" 
-plotFromDict
-------------
-plots logs stored in a dictionary
-- every key is one subplot, and it's value
-  is what you plot
-""" 
 def plotFromDict(plot_data, final_iteration, args):
+    """ 
+    plotFromDict
+    ------------
+    plots logs stored in a dictionary
+    - every key is one subplot, and it's value
+      is what you plot
+    """ 
     # TODO: replace with actual time ( you know what dt is from args)
     t = np.arange(final_iteration)
     n_cols, n_rows = getNRowsMColumnsFromTotalNumber(len(plot_data))