diff --git a/python/ur_simple_control/visualize/__pycache__/make_run.cpython-310.pyc b/python/ur_simple_control/visualize/__pycache__/make_run.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..cc59155c44e721a1377bb4db4c29fbecfe833313
Binary files /dev/null and b/python/ur_simple_control/visualize/__pycache__/make_run.cpython-310.pyc differ
diff --git a/python/ur_simple_control/visualize/arms/another_debug_dh b/python/ur_simple_control/visualize/arms/another_debug_dh
new file mode 100644
index 0000000000000000000000000000000000000000..ab4e0ac1d82966d5d3e8fc5f4f18a7b0c95f6051
--- /dev/null
+++ b/python/ur_simple_control/visualize/arms/another_debug_dh
@@ -0,0 +1,9 @@
+0.0;0.0;1.0;0.0
+0.0;0.0;0.5;0.0
+0.0;0.0;1.0;0.0
+0.0;0.0;0.2;0.0
+0.0;0.0;1.0;0.0
+0.0;0.0;0.2;0.0
+0.0;0.0;1.0;0.0
+0.0;0.0;1.0;0.0
+0.0;0.0;1.0;0.0
diff --git a/python/ur_simple_control/visualize/arms/j2n6s300_dh_params b/python/ur_simple_control/visualize/arms/j2n6s300_dh_params
new file mode 100644
index 0000000000000000000000000000000000000000..44eed8ec8d3fca41fd835d50294c9ac7d9522732
--- /dev/null
+++ b/python/ur_simple_control/visualize/arms/j2n6s300_dh_params
@@ -0,0 +1,6 @@
+0.2766;0.0;0.0;1.5707966
+0.0;0.0;0.41;3.14159265
+-0.0098;0.0;0.0;1.57079632
+-0.25008;0.0;0.0;1.04719755
+-0.085563;0.0;0.0;1.04719755
+-0.202781;0.0;0.0;3.1415926
diff --git a/python/ur_simple_control/visualize/arms/j2s7s300_dh_params b/python/ur_simple_control/visualize/arms/j2s7s300_dh_params
new file mode 100644
index 0000000000000000000000000000000000000000..7d1b3fa4cefb06d1be34cc16b5c4ae8b395e902a
--- /dev/null
+++ b/python/ur_simple_control/visualize/arms/j2s7s300_dh_params
@@ -0,0 +1,7 @@
+-0.2755;0.0;0.0;1.5708
+0.0;0.0;0.0;1.5708
+-0.41;0.0;0.0;1.5708
+-0.0098;0.0;0.0;1.5708
+-0.31110;0.0;0.0;1.5708
+0.0;0.0;0.0;1.5708
+-0.20380;0.0;0.0;3.14159
diff --git a/python/ur_simple_control/visualize/arms/kinova_jaco_params b/python/ur_simple_control/visualize/arms/kinova_jaco_params
new file mode 100644
index 0000000000000000000000000000000000000000..57b26c9fc0c38042e96ea97d7321c01f779bdd84
--- /dev/null
+++ b/python/ur_simple_control/visualize/arms/kinova_jaco_params
@@ -0,0 +1,7 @@
+-0.2755;;0;1.5707963
+0;;0;1.5707963
+-0.4100;;0;1.5707963
+-0.0098;;0;1.5707963
+-0.3111;;0;1.5707963
+0;;0;1.5707963
+-0.2638;;0;3.1415926
diff --git a/python/ur_simple_control/visualize/arms/kuka_lbw_iiwa_dh_params b/python/ur_simple_control/visualize/arms/kuka_lbw_iiwa_dh_params
new file mode 100644
index 0000000000000000000000000000000000000000..3411eae78232e0d97dfd535a99263ea674d5c408
--- /dev/null
+++ b/python/ur_simple_control/visualize/arms/kuka_lbw_iiwa_dh_params
@@ -0,0 +1,7 @@
+0.36;0.0;0.0;-1.5708
+0.0;0.0;0.0;1.5708
+0.42;0.0;0.0;1.5708
+0.0;0.0;0.0;-1.5708
+0.40;0.0;0.0;-1.5708
+0.0;0.0;0.0;1.5708
+0.126;0.0;0.0;0.0
diff --git a/python/ur_simple_control/visualize/arms/lbr_iiwa_jos_jedan_dh b/python/ur_simple_control/visualize/arms/lbr_iiwa_jos_jedan_dh
new file mode 100644
index 0000000000000000000000000000000000000000..69b01f64fc37b52ebcc200bcfcf9a05078b747de
--- /dev/null
+++ b/python/ur_simple_control/visualize/arms/lbr_iiwa_jos_jedan_dh
@@ -0,0 +1,7 @@
+0.34;0.0;0.0;-1.5708
+0.0;0.0;0.0;1.5708
+0.40;0.0;0.0;1.5708
+0.0;0.0;0.0;-1.5708
+0.40;0.0;0.0;-1.5708
+0.0;0.0;0.0;1.5708
+0.125;0.0;0.0;0.0
diff --git a/python/ur_simple_control/visualize/arms/robot_parameters2 b/python/ur_simple_control/visualize/arms/robot_parameters2
new file mode 100644
index 0000000000000000000000000000000000000000..6487a200a40cfbe40a79615772832b0931d02089
--- /dev/null
+++ b/python/ur_simple_control/visualize/arms/robot_parameters2
@@ -0,0 +1,9 @@
+.20;.02;.37;.00
+.00;.55;.91;.17
+.05;.39;.76;.00
+-.10;.10;.48;-.17
+.05;.19;.58;.00
+-.10;.10;.50;-.17
+.05;.19;.60;.00
+-.10;.10;.52;-.17
+.05;.19;.61999;.00
diff --git a/python/ur_simple_control/visualize/arms/testing_dh_parameters b/python/ur_simple_control/visualize/arms/testing_dh_parameters
new file mode 100644
index 0000000000000000000000000000000000000000..8c60750ade82902789ace9705c2001d310c32273
--- /dev/null
+++ b/python/ur_simple_control/visualize/arms/testing_dh_parameters
@@ -0,0 +1,6 @@
+0.1273;0.0;0.0;1.5708
+0.0;0.0;-0.612;0.0
+0.0;0.0;-0.5723;0.0
+0.1639;0.0;0.0;1.5708
+0.1157;0.0;0.0;-1.5708
+0.0922;0.0;0.0;0.0
diff --git a/python/ur_simple_control/visualize/arms/ur10e_dh_parameters_from_the_ur_site b/python/ur_simple_control/visualize/arms/ur10e_dh_parameters_from_the_ur_site
new file mode 100644
index 0000000000000000000000000000000000000000..991315d641fbffcf960ba9a33c0d71d0c4b3c829
--- /dev/null
+++ b/python/ur_simple_control/visualize/arms/ur10e_dh_parameters_from_the_ur_site
@@ -0,0 +1,6 @@
+0.1807;0.0;0;1.5707963
+0.0;0.0;-0.6127;0.0
+0.0;0.0;-0.57155;0.0
+0.17415;0.0;0.0;1.5707963
+0.11985;0.0;0.0;-1.5707963
+0.11655;0.0;0.0;0.0
diff --git a/python/ur_simple_control/visualize/arms/ur5e_dh b/python/ur_simple_control/visualize/arms/ur5e_dh
new file mode 100644
index 0000000000000000000000000000000000000000..09af7229e9e354153fc913f1f410af7cd8b41881
--- /dev/null
+++ b/python/ur_simple_control/visualize/arms/ur5e_dh
@@ -0,0 +1,6 @@
+0.163;0.0;0;1.5707963
+0.0;0.0;-0.4250;0.0
+0.0;0.0;-0.39225;0.0
+0.134;0.0;0.0;1.5707963
+0.100;0.0;0.0;-1.5707963
+0.100;0.0;0.0;0.0
diff --git a/python/ur_simple_control/visualize/main.py b/python/ur_simple_control/visualize/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..19b36264102f5a3a6e5d31bc079d1cee1815c609
--- /dev/null
+++ b/python/ur_simple_control/visualize/main.py
@@ -0,0 +1,78 @@
+# idc if i need this
+# Implement the default Matplotlib key bindings.
+from robot_stuff.InverseKinematics import InverseKinematicsEnv
+from robot_stuff.drawing import *
+from robot_stuff.inv_kinm import *
+
+import numpy as np
+import matplotlib.pyplot as plt
+import time
+
+from rtde_control import RTDEControlInterface as RTDEControl
+from rtde_receive import RTDEReceiveInterface as RTDEReceive
+
+#rtde_c = RTDEControl("192.168.1.102")
+rtde_c = RTDEControl("127.0.0.1")
+
+#rtde_r = RTDEReceive("192.168.1.102")
+rtde_r = RTDEReceive("127.0.0.1")
+q = rtde_r.getActualQ()
+
+# Parameters
+acceleration = 0.5
+dt = 1.0/500  # 2ms
+
+ik_env = InverseKinematicsEnv()
+ik_env.damping = 25
+ik_env.goal[0] = -0.21
+ik_env.goal[1] = -0.38
+ik_env.goal[2] = 0.2
+ik_env.render()
+
+
+ik_env.robot.setJoints(q)
+ik_env.robot.calcJacobian()
+print(ik_env.robot.p_e)
+#print(type(init_q))
+#exit()
+
+# putting it into this class so that python remembers it 'cos reasons, whatever
+controller = invKinm_dampedSquares
+#controller = invKinmSingAvoidanceWithQP_kI
+#while True:
+while True:
+    start = time.time()
+    q = rtde_r.getActualQ()
+    ik_env.robot.setJoints(q)
+    ik_env.robot.calcJacobian()
+    q_dots = controller(ik_env.robot, ik_env.goal)
+    thetas = np.array([joint.theta for joint in ik_env.robot.joints])
+    ik_env.render()
+    #print(ik_env.robot.p_e.copy())
+    #print(ik_env.goal)
+    distance = np.linalg.norm(ik_env.robot.p_e.copy() - ik_env.goal)
+    print(distance)
+    if distance < 0.01:
+        t_start = rtde_c.initPeriod()
+        t_start = rtde_c.initPeriod()
+        rtde_c.speedJ(np.zeros(6), acceleration, dt)
+        rtde_c.waitPeriod(t_start)
+        break
+
+#    print(q_dots)
+#    print(thetas)
+    
+    end = time.time()
+    print("time on rendering", end - start)
+    print("fps: ", 1/ (end - start))
+    t_start = rtde_c.initPeriod()
+    rtde_c.speedJ(q_dots, acceleration, dt)
+    rtde_c.waitPeriod(t_start)
+
+for i in range(20):
+    t_start = rtde_c.initPeriod()
+    rtde_c.speedJ(np.zeros(6), acceleration, dt)
+    rtde_c.waitPeriod(t_start)
+
+
+print("done")
diff --git a/python/ur_simple_control/visualize/make_run.py b/python/ur_simple_control/visualize/make_run.py
new file mode 100644
index 0000000000000000000000000000000000000000..240c7fd2440b06a8e325e790b5c2e32b8323fcb9
--- /dev/null
+++ b/python/ur_simple_control/visualize/make_run.py
@@ -0,0 +1,45 @@
+from robot_stuff.InverseKinematics import InverseKinematicsEnv
+from robot_stuff.drawing import *
+from robot_stuff.inv_kinm import *
+from robot_stuff.utils import *
+
+import numpy as np
+
+# starting off with random goals, who cares rn
+# policy is one of the inverse kinematics control algoritms
+# TODO (optional): make it follow a predefined trajectory
+# TODO (optional): vectorize it if it isn't fast enough (at least you avoid dynamic allocations)
+def makeRun(controller, ik_env, n_iters, robot_index):
+    # we'll shove everything into lists, and have it ready made
+    data = {
+    "qs" : np.zeros((n_iters, ik_env.robots[robot_index].ndof)),
+    "q_dots" : np.zeros((n_iters, ik_env.robots[robot_index].ndof)),
+    "manip_ell_eigenvals" : np.zeros((n_iters, 3)) ,
+    "manip_elip_svd_rots" : np.zeros((n_iters, 3, 3)),
+    "p_es" : np.zeros((n_iters, 3)),
+    "vs" : np.zeros((n_iters, 6)), # all links linear velocities # NOT USED ATM
+    "dists_to_goal" : np.zeros(n_iters),
+    "manip_indeces" : np.zeros(n_iters),
+    }
+
+    for i in range(n_iters):
+        q_dots = controller(ik_env.robots[robot_index], ik_env.goal)
+        ik_env.simpleStep(q_dots, 1.0, robot_index)
+
+        thetas = np.array([joint.theta for joint in ik_env.robots[robot_index].joints])
+        data['qs'][i] = thetas
+        data['q_dots'][i] = q_dots
+        # NOTE: this is probably not correct, but who cares
+        data['vs'][i] = ik_env.robots[robot_index].jacobian @ q_dots
+        M = ik_env.robots[robot_index].jac_tri @ ik_env.robots[robot_index].jac_tri.T
+        manip_index = np.sqrt(np.linalg.det(M))
+        data['manip_indeces'][i] = manip_index
+        _, diag_svd, rot = np.linalg.svd(M)
+        # TODO check this: idk if this or the square roots are eigenvalues 
+        data["manip_ell_eigenvals"][i] = np.sqrt(diag_svd)
+        data["manip_elip_svd_rots"][i] = rot
+        smallest_eigenval = diag_svd[diag_svd.argmin()]
+        dist_to_goal = np.linalg.norm(ik_env.robots[robot_index].p_e - ik_env.goal)
+        data['dists_to_goal'][i] = dist_to_goal
+        data['p_es'][i] = ik_env.robots[robot_index].p_e
+    return data
diff --git a/python/ur_simple_control/visualize/manipulator_visual_motion_analyzer.py b/python/ur_simple_control/visualize/manipulator_visual_motion_analyzer.py
new file mode 100644
index 0000000000000000000000000000000000000000..11fb982c8d388f5d0d44f2e45df0f167e2d34e82
--- /dev/null
+++ b/python/ur_simple_control/visualize/manipulator_visual_motion_analyzer.py
@@ -0,0 +1,709 @@
+# override ugly tk widgets with system-based ttk ones
+# change the import * thing if it's better for your ide
+from tkinter import * 
+from tkinter.ttk import *
+
+from matplotlib.backends.backend_tkagg import (
+    FigureCanvasTkAgg, NavigationToolbar2Tk)
+import matplotlib.pyplot as plt
+# Implement the default Matplotlib key bindings.
+from matplotlib.backend_bases import key_press_handler
+from matplotlib.figure import Figure
+import pyautogui
+from matplotlib.gridspec import GridSpec
+
+from robot_stuff.InverseKinematics import InverseKinematicsEnv
+from robot_stuff.drawing import *
+from robot_stuff.inv_kinm import *
+from make_run import makeRun
+
+import numpy as np
+import sys
+# needed to communicate with the gui in real time
+from multiprocessing import Queue
+# don't want to refactor yet, but obv
+# TODO: refactor, have normal names for things
+# it's just for getting the fps reading while optimizing
+import time as ttime
+
+
+
+# TODO: call update_point function with the current slider value after updating 
+# stuff like eliipse on/off so that you don't need to move the button to get it
+# just call the slider string value to get the current slider value
+# TODO: finish writing up reseting to the same starting position (joint values)
+
+
+#######################################################################
+#                            CONTROL STUFF                            #
+#######################################################################
+
+# TODO: remove this/import it from clik
+def getController(controller_name):
+    
+    if controller_name == "invKinmQPSingAvoidE_kI":
+        return invKinmQPSingAvoidE_kI
+    if controller_name == "invKinm_Jac_T":
+        return invKinm_Jac_T
+    if controller_name == "invKinm_PseudoInv":
+        return invKinm_PseudoInv
+    if controller_name == "invKinm_dampedSquares":
+        return invKinm_dampedSquares
+    if controller_name == "invKinm_PseudoInv_half":
+        return invKinm_PseudoInv_half
+    if controller_name == "invKinmQP":
+        return invKinmQP
+    if controller_name == "invKinmQPSingAvoidE_kI":
+        return invKinmQPSingAvoidE_kI
+    if controller_name == "invKinmQPSingAvoidE_kM":
+        return invKinmQPSingAvoidE_kM
+    if controller_name == "invKinmQPSingAvoidManipMax":
+        return invKinmQPSingAvoidManipMax
+
+    return invKinm_dampedSquares
+
+"""
+GetCommandThread
+-----------------
+- NOTE: NOT USED ATM
+- requires separate thread to accomodate the updating
+  weirdness of tkinter. 
+- i know how this works, so i'm running with it,
+  if you want to make it better, be my guest, code it up
+- TODO: make it work for the new application
+--> just send q commands here
+- there are 2 queues because i didn't know what i was doing honestly,
+  but hey, it works and it ain't hurting nobody
+- the point is that you generate an event for the gui, and then get what
+  that event actually was by poping from the queue.
+  and you can shove anything, ex. whole dicts into the queue because python
+"""
+class GetCommandThread(threading.Thread):
+    def __init__(self, queue, localQueue, _check):
+        super(GetCommandThread, self).__init__()
+        self.queue = queue
+        self.localQueue = localQueue
+        self._check = _check
+
+    def run(self):
+        #print("worker thread doing stuff")
+        commands = self.queue.get()
+        # maybe just wait untill it's non-empty with another blocking call?
+        # then .get() from the main thread
+        self.localQueue.put(commands)
+#        if random.randint(0, 1) == 0:
+#            self.localQueue.put({'set_image': 'smile', 'set_text': "ready to work" })
+#        else:
+#            self.localQueue.put({'set_image': 'thumb_up', 'set_text': "updated" })
+
+        if self._check.get() == 0:
+            self._check.set(1)
+        #    print('registered new callback')
+        else:
+            self._check.set(0)
+        #    print('registered new callback')
+
+
+"""
+ManipulatorVisualMotionAnalyzer
+----------------------------------
+- for now leaving run generation here for easier testing
+- later load run and run on that
+- add option to load run while the robot is running
+- some possibly unused stuff will be added here as a weird transplant
+  from an old project, but will be trimmed later
+"""
+# BIG TODO:
+# shove all artists to be updated into a single list called updating_artists
+# and then update all of them in a single for loop.
+# shove all other non-updating artists into a single list.
+# this list is to be updated only if you load a new run.
+# or even skip this and reload everything, who cares.
+# NOTE: better alternative: shove them into dicts, not lists,
+# so that they have names. the for loop functions the same way,
+# but you get to know what you have, which might be useful later.
+class ManipulatorVisualMotionAnalyzer:
+    def __init__(self, root, queue, data=data):
+        # need to put this in the main program, 
+        # so you need to pass it here to use it
+        self.root = root
+        self.root.wm_title("Embedding in Tk")
+        # for real-time updates
+        self.queue = queue
+        #######################
+        #  visual parameters  #
+        #######################
+        self.DPI = 80
+        self.SLIDER_SIZE = 200
+        #DPI = 200
+        screensize = pyautogui.size()
+        self.SCREEN_WIDTH = screensize.width
+        self.SCREEN_HEIGHT = screensize.height
+        #self.SCALING_FACTOR_HEIGHT = 0.8
+        #self.SCALING_FACTOR_WIDTH = 0.5
+        self.SCALING_FACTOR_HEIGHT = 0.3
+        self.SCALING_FACTOR_WIDTH = 0.3
+
+        #####################################################
+        #  LAYOUT OF THE GUI                                #
+        # putting plots into: frames -> notebooks -> tabs   #
+        #####################################################
+        self.notebook_left = Notebook(root, height=int(SCREEN_HEIGHT))
+        self.notebook_right = Notebook(root, height=int(SCREEN_HEIGHT))
+        self.frame_manipulator = Frame(notebook_left)
+        self.frame_manip_graphs = Frame(notebook_right)
+        self.frame_imu = Frame(notebook_right)
+        self.frame_ee = Frame(notebook_left)
+        self.tabs_left = notebook_left.add(frame_manipulator, text='manipulator')
+        self.tabs_left = notebook_left.add(frame_ee, text="end-effector")
+        self.tabs_right = notebook_right.add(frame_manip_graphs, text='manipulator-graphs')
+        self.tabs_right = notebook_right.add(frame_imu, text="ee-graphs")
+        self.notebook_left.grid(row=0, column=0, sticky='ew')
+        self.notebook_right.grid(row=0, column=1, sticky='ew')
+        
+        ########################
+        #  run related things  #
+        ########################
+        # TODO: remove from here,
+        # we won't be generating runs here later
+        self.n_iters = 200
+        # the word time is used for timing
+        self.t = np.arange(n_iters)
+        # local kinematics integrator
+        # but this thing handles plotting
+        # only plotting needs to be run, so feel free to 
+        # TODO butcher this later on.
+        # NOTE: this thing holds axes and artists.
+        # make this class hold them, because then it can manage
+        # all of them in all figures, this is scitzo af bruv
+        self.ik_env = InverseKinematicsEnv()
+        # TODO: do this depending on the number of runs you'll be loading
+        #self.ik_env.robots.append(Robot_raw(robot_name="no_sim"))
+        self.ik_env.damping = 25
+        # putting it into this class so that python remembers it 'cos reasons, whatever
+        # TODO: load this from run log files later
+        # or just remove it since you're not generating runs from here
+        self.controller1 = getController("")
+        self.controller2 = getController("invKinm_Jac_T")
+        # TODO: load runs, not make them
+        # but deliver the same format.
+        # TODO: ensure you're saving AT LEAST what's required here
+        # NOTE: the jacobian svd is computed offline in these
+        self.ik_env.data.append(makeRun(controller1, ik_env, n_iters, 0))
+        self.ik_env.data.append(makeRun(controller2, ik_env, n_iters, 1))
+
+
+
+        # how to add plots will be documented below on an easier example
+        #######################################################################
+        #                             UNUSED PLOT                             #
+        #######################################################################
+        self.fig_ee = Figure(figsize=(SCREEN_WIDTH/DPI*SCALING_FACTOR_WIDTH, SCREEN_HEIGHT/DPI*SCALING_FACTOR_HEIGHT), dpi=DPI)
+        self.rec3D_ax = self.fig_ee.add_subplot(projection='3d') 
+        self.rec3D_ax.set(xticklabels=[], yticklabels=[], zticklabels=[])
+
+        #######################################################################
+        #                          MANIPULATOR PLOT                           #
+        #######################################################################
+        # robot plot
+        self.fig_manipulator = Figure(figsize=(SCREEN_WIDTH/DPI*SCALING_FACTOR_WIDTH, SCREEN_HEIGHT/DPI*SCALING_FACTOR_HEIGHT), dpi=DPI)
+        self.ik_env.ax = self.fig_manipulator.add_subplot(projection='3d') 
+        # fix array sizes to be the workspace as that won't change. otherwise it's updated as you go,
+        # and that's impossible to look at.
+        self.ik_env.ax.plot(np.array([0]), np.array([0]), np.array([1.5]), c='b')
+        self.ik_env.ax.plot(np.array([0]), np.array([0]), np.array([-1.5]), c='b')
+        self.ik_env.ax.set_xlim([-1.0,1.0])
+        self.ik_env.ax.set_ylim([-1.0,1.0])
+        # TODO: generate this from a colormap
+        self.link_colors = ['black', 'violet']
+        self.trajectory_plots = []
+        # this way you can have more robots plotted and see the differences between algorithms
+        # but it of course works on a single robot too
+        for robot_index, robot in enumerate(self.ik_env.robots):
+            robot.initDrawing(self.ik_env.ax, self.link_colors[robot_index])
+            # ee point
+            # TODO: blit the point because it moves
+            self.ik_env.p_e_point_plots.append(drawPoint(ik_env.ax, ik_env.data[robot_index]['p_es'][0], 'red', 'o'))
+            # plot ee position trajectory. not blitted because it is fixed
+            # TODO: blit it for real-time operation
+            self.trajectory_plot, = ik_env.ax.plot(ik_env.data[robot_index]['p_es'][:,0], ik_env.data[robot_index]['p_es'][:,1], ik_env.data[robot_index]['p_es'][:,2], color='blue')
+            trajectory_plots.append(trajectory_plot)
+        # goal point
+        # only makes sense for clik, but keep it here anyway, it doesn't hurt
+        # TODO: add a button to turn it off
+        self.ik_env.goal_point_plot = drawPoint(ik_env.ax, ik_env.goal, 'maroon', '*')
+
+        # the manipulability ellipses
+        # TODO: BLIT THEM
+        for robot_index, robot in enumerate(self.ik_env.robots):
+            radii = 1.0/self.ik_env.data[robot_index]["manip_ell_eigenvals"][0] * 0.1
+            u = np.linspace(0.0, 2.0 * np.pi, 60)     
+            v = np.linspace(0.0, np.pi, 60)     
+            x = radii[0] * np.outer(np.cos(u), np.sin(v))     
+            y = radii[1] * np.outer(np.sin(u), np.sin(v))     
+            z = radii[2] * np.outer(np.ones_like(u), np.cos(v))
+            for i in range(len(x)):         
+                for j in range(len(x)):
+                    # TODO: add if to compute this in real-time if you're running real-time
+                    # although, again, primary purpose is to log stuff for later
+                    [x[i,j],y[i,j],z[i,j]] = np.dot([x[i,j],y[i,j],z[i,j]], self.ik_env.data[robot_index]["manip_elip_svd_rots"][0]) + self.ik_env.data[robot_index]['p_es'][0]
+
+            self.ik_env.ellipsoid_surf_plots.append(self.ik_env.ax.plot_surface(x, y, z,  rstride=3, cstride=3,
+                            color='pink', linewidth=0.1,
+                            alpha=0.3, shade=True))
+
+        #######################################################################
+        #                        manipulability plots                         #
+        #######################################################################
+        # manipulability ellipsoid eigenvalues
+        self.fig_manip_graphs = Figure(figsize=(SCREEN_WIDTH/DPI*SCALING_FACTOR_WIDTH, SCREEN_HEIGHT/DPI*SCALING_FACTOR_HEIGHT), dpi=DPI)
+        # NOTE: this ax object lives on in some other matplotlib object
+        # so i don't have to save it
+        ax = self.fig_manip_graphs.add_subplot(311)
+        ax.set_title('Manipulability ellipsoid eigenvalues')
+        ax.grid()
+        ax.set_xticks([])
+        self.eigen1_lines = []
+        self.eigen2_lines = []
+        self.eigen3_lines = []
+        for robot_index, robot in enumerate(ik_env.robots):
+            eigen1_line, = ax.plot(time, ik_env.data[robot_index]["manip_ell_eigenvals"][:,0], color=link_colors[robot_index])
+            self.eigen1_lines.append(eigen1_line)
+            eigen2_line, = ax.plot(time, ik_env.data[robot_index]["manip_ell_eigenvals"][:,1], color=link_colors[robot_index])
+            self.eigen2_lines.append(eigen2_line)
+            eigen3_line, = ax.plot(time, ik_env.data[robot_index]["manip_ell_eigenvals"][:,2], color=link_colors[robot_index])
+            self.eigen3_lines.append(eigen3_line)
+        # TODO: blit this line
+        self.point_in_time_eigen1_line = ax.axvline(x=0, color='red')
+
+        # manipulability index (volume of manipulability ellipsoid)
+        ax = self.fig_manip_graphs.add_subplot(312) 
+        ax.set_title('Manipulability index')
+        ax.grid()
+        ax.set_xticks([])
+        self.manip_index_lines = []
+        for robot_index, robot in enumerate(ik_env.robots):
+            manip_index_line, = ax.plot(time, ik_env.data[robot_index]['manip_indeces'], color=link_colors[robot_index])
+            self.manip_index_lines.append(manip_index_line)
+        self.point_in_time_manip_index_line = ax.axvline(x=0, color='red')
+
+        # dist to goal (this could be/should be elsewhere)
+        ax = self.fig_manip_graphs.add_subplot(313) 
+        ax.set_title('Distance to goal')
+        ax.grid()
+        ax.set_xlabel('iter')
+        self.dist_to_goal_lines = []
+        for robot_index, robot in enumerate(ik_env.robots):
+            self.dist_to_goal_line, = ax.plot(time, ik_env.data[robot_index]['dists_to_goal'], color=link_colors[robot_index])
+            dist_to_goal_lines.append(dist_to_goal_line)
+        # TODO: blit this line
+        self.point_in_time_dist_to_goal_line = ax.axvline(x=0, color='red')
+
+
+        #######################################################################
+        #                              IMU plots                              #
+        #######################################################################
+        self.fig_imu = Figure(figsize=(SCREEN_WIDTH/DPI*SCALING_FACTOR_WIDTH, SCREEN_HEIGHT/DPI*SCALING_FACTOR_HEIGHT), dpi=DPI)
+        self.v_x_lines = []
+        self.v_y_lines = []
+        self.v_z_lines = []
+        self.omega_x_lines = []
+        self.omega_y_lines = []
+        self.omega_z_lines = []
+
+        ax_v_x = self.fig_imu.add_subplot(321)
+        ax_v_x.grid()
+        ax_v_y = self.fig_imu.add_subplot(322)
+        ax_v_y.grid()
+        ax_v_z = self.fig_imu.add_subplot(323)
+        ax_v_z.grid()
+        ax_omega_x = self.fig_imu.add_subplot(324)
+        ax_omega_x.grid()
+        ax_omega_y = self.fig_imu.add_subplot(325)
+        ax_omega_y.grid()
+        ax_omega_z = self.fig_imu.add_subplot(326)
+        ax_omega_z.grid()
+        ax_v_x.set_title('Linear velocity x')
+        ax_v_y.set_title('Linear velocity y')
+        ax_v_z.set_title('Linear velocity z')
+        ax_omega_x.set_title('Angular velocity x')
+        ax_omega_y.set_title('Angular velocity y')
+        ax_omega_z.set_title('Angular velocity z')
+        ax_v_x.set_xticks([])
+        ax_v_y.set_xticks([])
+        ax_v_z.set_xticks([])
+        ax_omega_x.set_xticks([])
+        ax_omega_y.set_xticks([])
+        ax_omega_z.set_xticks([])
+        for robot_index, robot in enumerate(ik_env.robots):
+            v_x_line, = ax_v_x.plot(self.t, self.ik_env.data[robot_index]["vs"][:,0], color=self.link_colors[robot_index])
+            v_y_line, = ax_v_y.plot(self.t, self.ik_env.data[robot_index]["vs"][:,1], color=self.link_colors[robot_index])
+            v_z_line, = ax_v_z.plot(self.t, self.ik_env.data[robot_index]["vs"][:,2], color=self.link_colors[robot_index])
+            omega_x_line, = ax_omega_x.plot(self.t, self.ik_env.data[robot_index]["vs"][:,3], color=self.link_colors[robot_index])
+            omega_y_line, = ax_omega_y.plot(self.t, self.ik_env.data[robot_index]["vs"][:,4], color=self.link_colors[robot_index])
+            omega_z_line, = ax_omega_z.plot(self.t, self.ik_env.data[robot_index]["vs"][:,5], color=self.link_colors[robot_index])
+            self.v_x_lines.append(v_x_line)
+            self.v_y_lines.append(v_y_line)
+            self.v_z_lines.append(v_z_line)
+            self.omega_x_lines.append(omega_x_line)
+            self.omega_y_lines.append(omega_y_line)
+            self.omega_z_lines.append(omega_z_line)
+
+        # TODO: blit these
+        self.point_in_time_ax_v_x_line = ax_v_x.axvline(x=0, color='red')
+        self.point_in_time_ax_v_y_line = ax_v_y.axvline(x=0, color='red')
+        self.point_in_time_ax_v_z_line = ax_v_z.axvline(x=0, color='red')
+        self.point_in_time_ax_omega_x_line = ax_omega_x.axvline(x=0, color='red')
+        self.point_in_time_ax_omega_y_line = ax_omega_y.axvline(x=0, color='red')
+        self.point_in_time_ax_omega_z_line = ax_omega_z.axvline(x=0, color='red')
+
+
+
+# tkinterize these plots
+canvas_manipulator = FigureCanvasTkAgg(fig_manipulator, master=frame_manipulator) 
+canvas_manipulator.draw()
+# NEW
+# TODO maybe you want another background idk
+# worked elsewhere with figure.bbox
+background_manipulator = canvas_manipulator.copy_from_bbox(fig_manipulator.bbox)
+#background_manipulator = canvas_manipulator.copy_from_bbox(canvas_manipulator.bbox)
+
+canvas_manipulator_widget = canvas_manipulator.get_tk_widget()     
+canvas_manipulator_widget.grid(row=0, column=0) 
+canvas_manipulator._tkcanvas.grid(row=1, column=0)   
+canvas_manipulator.draw()
+
+canvas_ee = FigureCanvasTkAgg(fig_ee, master=frame_ee) 
+canvas_ee_widget = canvas_ee.get_tk_widget()     
+canvas_ee_widget.grid(row=0, column=0) 
+canvas_ee._tkcanvas.grid(row=1, column=0)   
+canvas_ee.draw()
+
+canvas_manip_graphs = FigureCanvasTkAgg(fig_manip_graphs, master=frame_manip_graphs) 
+canvas_manip_graphs_widget = canvas_manip_graphs.get_tk_widget()     
+canvas_manip_graphs_widget.grid(row=0, column=0) 
+canvas_manip_graphs._tkcanvas.grid(row=1, column=0)   
+canvas_manip_graphs.draw()
+
+canvas_imu = FigureCanvasTkAgg(self.fig_imu, master=frame_imu) 
+canvas_imu_widget = canvas_manip_graphs.get_tk_widget()     
+canvas_imu_widget.grid(row=0, column=0) 
+canvas_imu._tkcanvas.grid(row=1, column=0)   
+canvas_imu.draw()
+
+# pack_toolbar=False will make it easier to use a layout manager later on.
+toolbar_manipulator = NavigationToolbar2Tk(canvas_manipulator, frame_manipulator, pack_toolbar=False)
+toolbar_manipulator.update()
+toolbar_manipulator.grid(column=0, row=2)
+toolbar_manip_graphs = NavigationToolbar2Tk(canvas_manip_graphs, frame_manip_graphs, pack_toolbar=False)
+toolbar_manip_graphs.update()
+toolbar_manip_graphs.grid(column=0, row=2)
+toolbar_ee = NavigationToolbar2Tk(canvas_ee, frame_ee, pack_toolbar=False)
+toolbar_ee.update()
+toolbar_ee.grid(column=0, row=2)
+toolbar_imu = NavigationToolbar2Tk(canvas_imu, frame_imu, pack_toolbar=False)
+toolbar_imu.update()
+toolbar_imu.grid(column=0, row=2)
+
+#######################################################################
+#                 functions for widgets to control the plots          #
+#######################################################################
+
+# keyboard inputs
+# whatever, doesn't hurt, could be used
+#canvas.mpl_connect(
+#    "key_press_event", lambda event: print(f"you pressed {event.key}"))
+#canvas.mpl_connect("key_press_event", key_press_handler)
+
+# new_val is what the widget gives you
+# you define what needs to happen to plots in Figure below,
+# and call canvas.draw
+def update_points(new_val):
+# ee plot
+###########################################################################
+    start = ttime.time()
+    #fig_manipulator.canvas.restore_region(background_manipulator)
+    canvas_manipulator.restore_region(background_manipulator)
+    index = int(np.floor(float(new_val)))
+    
+    # Update 3D reconstruction plot
+    # plot3D_camera(recP[index], scale=scale)
+#    C, Q = camera_data_for_plot3D(recP[cam_indices[index]])
+#    camera_center.set_data_3d(*C)
+#    C = C.flatten()
+#    for ii in range(len(camera_coordinate_system)):
+#        camera_coordinate_system[ii].set_data([C[0], C[0] + scale * Q[0,ii]],
+#                                              [C[1], C[1] + scale * Q[1,ii]])
+#        camera_coordinate_system[ii].set_3d_properties([C[2], C[2] + scale * Q[2,ii]])
+#    recX_ = recX[:,X_indices[:num_pts_subset*(index+1)]]
+#    rec_points3D.set_data_3d(recX_[0,:], recX_[1,:], recX_[2,:])
+###########################################################################
+
+    ik_env.ax.set_title(str(index) + 'th iteration toward goal')
+    # animate 3d manipulator
+    for robot_index, robot in enumerate(ik_env.robots):
+        ik_env.robots[robot_index].setJoints(ik_env.data[robot_index]["qs"][index])
+        ik_env.robots[robot_index].drawStateAnim()
+
+    # NEW AND BROKEN
+        for link in robot.lines:
+            for line in link:
+                ik_env.ax.draw_artist(line)
+        # all these are in lists as that's what the line plot wants, 
+        # despite the fact that we have single points
+        point_in_time_eigen1_line.set_xdata([time[index]])
+        point_in_time_manip_index_line.set_xdata([time[index]])
+        point_in_time_dist_to_goal_line.set_xdata([time[index]])
+        point_in_time_ax_v_x_line.set_xdata([time[index]])
+        point_in_time_ax_v_y_line.set_xdata([time[index]])
+        point_in_time_ax_v_z_line.set_xdata([time[index]])
+        point_in_time_ax_omega_x_line.set_xdata([time[index]])
+        point_in_time_ax_omega_y_line.set_xdata([time[index]])
+        point_in_time_ax_omega_z_line.set_xdata([time[index]])
+
+        # ellipsoid update
+        radii = 1.0/ik_env.data[robot_index]["manip_ell_eigenvals"][index] * 0.1
+        u = np.linspace(0.0, 2.0 * np.pi, 60)     
+        v = np.linspace(0.0, np.pi, 60)     
+        x = radii[0] * np.outer(np.cos(u), np.sin(v))     
+        y = radii[1] * np.outer(np.sin(u), np.sin(v))     
+        z = radii[2] * np.outer(np.ones_like(u), np.cos(v))
+        for i in range(len(x)):         
+            for j in range(len(x)):
+                [x[i,j],y[i,j],z[i,j]] = np.dot([x[i,j],y[i,j],z[i,j]], ik_env.data[robot_index]["manip_elip_svd_rots"][index]) + ik_env.data[robot_index]['p_es'][index]
+
+        if ellipse_on_off_var.get():
+            try:
+                ik_env.ellipsoid_surf_plots[robot_index].remove()
+            except ValueError:
+                pass
+            ik_env.ellipsoid_surf_plots[robot_index] = ik_env.ax.plot_surface(x, y, z,  rstride=3, cstride=3,
+                            color='pink', linewidth=0.1,
+                            alpha=0.3, shade=True) 
+
+        ik_env.p_e_point_plots[robot_index].set_data([ik_env.data[robot_index]['p_es'][index][0]], [ik_env.data[robot_index]['p_es'][index][1]])
+        ik_env.p_e_point_plots[robot_index].set_3d_properties([ik_env.data[robot_index]['p_es'][index][2]])
+    canvas_ee.draw()
+    # NEW AND BROKEN
+#    fig_manipulator.canvas.blit(fig_manipulator.bbox)
+#    fig_manipulator.canvas.flush_events()
+    canvas_manipulator.blit(fig_manipulator.bbox)
+    canvas_manipulator.flush_events()
+    #canvas_manipulator.draw()
+    # might need to manually update all artists here from ax_something
+    #canvas_manipulator.blit(fig_manipulator.bbox)
+    canvas_manip_graphs.draw()
+    canvas_imu.draw()
+    # TODO update may not be needed as we're going by slider here
+    root.update()
+    end = ttime.time()
+    print("time per update:", end - start)
+    print("fps", 1 / (end - start))
+
+
+def update_goal_x(new_val):
+    goal_x = float(new_val)
+    ik_env.goal[0] = goal_x
+    ik_env.goal_point_plot.set_data([ik_env.goal[0]], [ik_env.goal[1]])
+    ik_env.goal_point_plot.set_3d_properties([ik_env.goal[2]])
+    canvas_ee.draw()
+    canvas_manipulator.draw()
+    canvas_manip_graphs.draw()
+    # TODO update may not be needed as we're going by slider here
+    root.update()
+
+def update_goal_y(new_val):
+    goal_y = float(new_val)
+    ik_env.goal[1] = goal_y
+    ik_env.goal_point_plot.set_data([ik_env.goal[0]], [ik_env.goal[1]])
+    ik_env.goal_point_plot.set_3d_properties([ik_env.goal[2]])
+    canvas_ee.draw()
+    canvas_manipulator.draw()
+    canvas_manip_graphs.draw()
+    # TODO update may not be needed as we're going by slider here
+    root.update()
+
+def update_goal_z(new_val):
+    goal_z = float(new_val)
+    ik_env.goal[2] = goal_z
+    ik_env.goal_point_plot.set_data([ik_env.goal[0]], [ik_env.goal[1]])
+    ik_env.goal_point_plot.set_3d_properties([ik_env.goal[2]])
+    canvas_ee.draw()
+    canvas_manipulator.draw()
+    canvas_manip_graphs.draw()
+    # TODO update may not be needed as we're going by slider here
+    root.update()
+
+
+
+def reset():
+    ik_env.reset()
+#    ik_env.goal_point_plot.remove()
+#    ik_env.goal_point_plot = drawPoint(ik_env.ax, ik_env.goal, 'red', 'o')
+#    print(controller_string1.get())
+#    print(controller_string2.get())
+    controller1 = getController(controller_string1.get())
+    controller2 = getController(controller_string2.get())
+    controllers = [controller1, controller2]
+    for robot_index, robot in enumerate(ik_env.robots):
+        ik_env.data.append(makeRun(controllers[robot_index], ik_env, n_iters, robot_index))
+        trajectory_plots[robot_index].set_data(ik_env.data[robot_index]['p_es'][:,0], ik_env.data[robot_index]['p_es'][:,1])
+        trajectory_plots[robot_index].set_3d_properties(ik_env.data[robot_index]['p_es'][:,2])
+        eigen1_lines[robot_index].set_ydata(ik_env.data[robot_index]["manip_ell_eigenvals"][:,0])
+        eigen2_lines[robot_index].set_ydata(ik_env.data[robot_index]["manip_ell_eigenvals"][:,1])
+        eigen3_lines[robot_index].set_ydata(ik_env.data[robot_index]["manip_ell_eigenvals"][:,2])
+        manip_index_lines[robot_index].set_ydata(ik_env.data[robot_index]['manip_indeces'])
+        dist_to_goal_lines[robot_index].set_ydata(ik_env.data[robot_index]['dists_to_goal'])
+    update_points(0)
+    canvas_ee.draw()
+    canvas_manipulator.draw()
+    canvas_manip_graphs.draw()
+    root.update()
+
+def play():
+    pass
+
+# ellipse on/off
+def add_remove_ellipse():
+    try:
+        for robot_index, robot in enumerate(ik_env.robots):
+            ik_env.ellipsoid_surf_plots[robot_index].remove()
+    except ValueError:
+        pass
+
+
+# NOTE: this thing is not used
+same_starting_position_on_off_var = IntVar()   
+same_starting_position_on_off_var.set(0)
+same_starting_position_checkbutton= Checkbutton(root, text = "same starting position on/off",
+                      variable = same_starting_position_on_off_var,
+                      onvalue = 1,
+                      offvalue = 0,
+#                      command=same_starting_position_cmd,
+                      )
+
+
+#######################################################################
+#                     PLACING ELEMENTS IN THE GUI                     #
+#######################################################################
+
+# LEFT PANE BELOW MANIP/EE PLOTS
+frame_below_manipulator_plot = Frame(frame_manipulator)
+frame_below_manipulator_plot.grid(column=0, row=3)
+
+# set controller 1
+frame_controller_menu1 = Frame(frame_below_manipulator_plot)
+controller_string1 = StringVar(frame_controller_menu1) 
+controller_string1.set("invKinm_dampedSquares") # default value 
+controller_menu1 = OptionMenu(frame_controller_menu1, controller_string1, 
+                "invKinmQPSingAvoidE_kI",
+                "invKinm_Jac_T",
+                "invKinm_PseudoInv",
+                "invKinm_dampedSquares",
+                "invKinm_PseudoInv_half",
+                "invKinmQP",
+                "invKinmQPSingAvoidE_kI",
+                "invKinmQPSingAvoidE_kM",
+                "invKinmQPSingAvoidManipMax",
+               ) 
+controller_string1.set("invKinm_dampedSquares")
+controller_menu1.grid(column=1, row=0)
+Label(frame_controller_menu1, text="Robot 1 controller:", background='yellow').grid(row=0, column=0, pady=4, padx = 4)
+frame_controller_menu1.grid(column=0, row=0)
+
+# set controller 2
+frame_controller_menu2 = Frame(frame_below_manipulator_plot)
+controller_string2 = StringVar(frame_controller_menu2) 
+controller_menu2 = OptionMenu(frame_controller_menu2, controller_string2, 
+                "invKinmQPSingAvoidE_kI",
+                "invKinm_Jac_T",
+                "invKinm_PseudoInv",
+                "invKinm_dampedSquares",
+                "invKinm_PseudoInv_half",
+                "invKinmQP",
+                "invKinmQPSingAvoidE_kI",
+                "invKinmQPSingAvoidE_kM",
+                "invKinmQPSingAvoidManipMax",
+               ) 
+controller_string2.set("invKinm_Jac_T") # default value 
+Label(frame_controller_menu1, text="Robot 1 controller:", background='black', foreground='white').grid(row=0, column=0, pady=4, padx = 4)
+controller_menu2.grid(column=1, row=0)
+Label(frame_controller_menu2, text="Robot 2 controller:", background='#EE82EE').grid(row=0, column=0, pady=4, padx = 4)
+frame_controller_menu2.grid(column=0, row=1)
+
+
+ellipse_on_off_var = IntVar()   
+ellipse_on_off_var.set(1)
+
+ellipse_checkbutton= Checkbutton(frame_below_manipulator_plot, text = "ellipse on/off",
+                      variable = ellipse_on_off_var,
+                      onvalue = 1,
+                      offvalue = 0,
+                      command=add_remove_ellipse,
+                      )
+ellipse_checkbutton.grid(column=1, row=0)
+
+
+frame_goal_x = Frame(frame_below_manipulator_plot)
+Label(frame_goal_x, text="goal x").grid(row=0, column=0, pady=4, padx = 4)
+slider_goal_x = Scale(frame_goal_x, from_=-1.0, to=1.0, length=SLIDER_SIZE, orient=HORIZONTAL,
+                              command=update_goal_x)
+slider_goal_x.grid(column=1, row=0)
+frame_goal_x.grid(column=1, row=1)
+
+frame_goal_y = Frame(frame_below_manipulator_plot)
+Label(frame_goal_y, text="goal y").grid(row=0, column=0, pady=4, padx = 4)
+slider_goal_y = Scale(frame_goal_y, from_=-1.0, to=1.0, length=SLIDER_SIZE, orient=HORIZONTAL,
+                              command=update_goal_y)
+slider_goal_y.grid(column=1, row=0)
+frame_goal_y.grid(column=1, row=2)
+
+
+frame_goal_z = Frame(frame_below_manipulator_plot)
+Label(frame_goal_z, text="goal z").grid(row=0, column=0, pady=4, padx = 4)
+slider_goal_z = Scale(frame_goal_z, from_=-1.0, to=1.0, length=SLIDER_SIZE, orient=HORIZONTAL,
+                              command=update_goal_z)
+slider_goal_z.grid(column=1, row=0)
+frame_goal_z.grid(column=1, row=3)
+
+
+frame_update = Frame(frame_manip_graphs)
+Label(frame_update, text="Time").grid(row=0, column=0, pady=4, padx = 4)
+slider_update = Scale(frame_update, from_=0, to=n_iters - 1, length=SLIDER_SIZE, orient=HORIZONTAL,
+                              command=update_points)#, label="Frequency [Hz]")
+slider_update.grid(column=1, row=0)
+frame_update.grid(column=0, row=3)
+
+frame_update2 = Frame(frame_imu)
+Label(frame_update2, text="Time").grid(row=0, column=0, pady=4, padx = 4)
+slider_update = Scale(frame_update2, from_=0, to=n_iters - 1, length=SLIDER_SIZE, orient=HORIZONTAL,
+                              command=update_points)#, label="Frequency [Hz]")
+slider_update.grid(column=1, row=0)
+frame_update2.grid(column=0, row=3)
+
+
+button_quit = Button(master=frame_manip_graphs, text="Quit", command=root.destroy)
+button_reset = Button(master=frame_manip_graphs, text="New run", command=reset)
+button_reset.grid(column=0, row=4)
+button_quit.grid(column=0, row=5)
+
+button_quit2 = Button(master=frame_imu, text="Quit", command=root.destroy)
+button_reset2 = Button(master=frame_imu, text="New run", command=reset)
+button_reset2.grid(column=0, row=4)
+button_quit2.grid(column=0, row=5)
+
+update_points(0)
+slider_goal_x.set(ik_env.goal[0])
+slider_goal_y.set(ik_env.goal[1])
+slider_goal_z.set(ik_env.goal[2])
+
+if name == "__main__":
+    queue = Queue()
+    root = Tk()
+    gui = ManipulatorVisualMotionAnalyzer(root, queue)
+
+    # have it 'cos from tkinter import *
+    mainloop()
+    # alternative if someone complains
+    #root.mainloop()
+
diff --git a/python/ur_simple_control/visualize/robot_stuff/InverseKinematics.py b/python/ur_simple_control/visualize/robot_stuff/InverseKinematics.py
new file mode 100644
index 0000000000000000000000000000000000000000..9cd579d94725f7e951d9a585bbc5869f95604d50
--- /dev/null
+++ b/python/ur_simple_control/visualize/robot_stuff/InverseKinematics.py
@@ -0,0 +1,226 @@
+# kinematics related imports
+from robot_stuff.forw_kinm import *
+from robot_stuff.inv_kinm import *
+from robot_stuff.follow_curve import *
+from robot_stuff.utils import *
+
+# general imports
+import numpy as np
+import matplotlib.pyplot as plt
+
+# ######################################### NOTE #########################
+# i'm half-adding another robot for the purposes of the plot.
+# this means that half of the code will end up being broken (as a lot of it is already anyway).
+# in case you want to use this elsewhere, just find the commit before the change
+# and run with that.
+##################################################################
+
+
+class InverseKinematicsEnv:
+
+# set damping (i.e. dt i.e. precision let's be real)
+    def __init__(self, model_path=None, initial_qpos=None, n_actions=None, n_substeps=None ):
+        print('env created')
+        self.chill_reset = False
+        # TODO write a convenience dh_parameter loading function
+        self.robot = Robot_raw(robot_name="no_sim")
+        self.robots = [self.robot]
+        self.init_qs_robot = []
+        self.damping = 5
+        self.error_vec = None
+        self.n_of_points_done = 0
+        # keep track of the timesteps (to be reset after every episode)
+        self.n_of_tries_for_point = 0
+        # for storing whole runs of all robots
+        self.data = []
+        self.p_e_point_plots = []
+        self.ellipsoid_surf_plots = []
+        self.same_starting_position_on_off = 0
+
+        # init goal
+        self.goal = np.random.random(3) * 0.7
+        # needed for easy initialization of the observation space
+
+        # TODO enable setting the other one with greater ease
+        self.reward_type = 'dense'
+        #self.reward_type = 'sparse'
+        self.episode_score = 0
+
+
+# NOTE: BROKEN AS IT'S FOR ONLY ONE ROBOT
+    def compute_reward(self, achieved_goal, goal, info):
+        # Compute distance between goal and the achieved goal.
+        if self.reward_type == 'sparse':
+            if not error_test(self.robot.p_e, self.goal):
+                return np.float32(-1.0)
+            else:
+                return np.float32(10.0)
+        if self.reward_type == 'dense':
+            distance = goal_distance(achieved_goal, goal)
+            if not error_test(self.robot.p_e, self.goal):
+                #reward = -1 * distance + 1 / distance
+                reward = -1 * distance 
+            else:
+                reward = 100
+            return reward
+            
+
+
+    def simpleStep(self, q_dots, damping, index):
+        self.robot.forwardKinmViaPositions(q_dots / self.damping, damping)
+        self.n_of_tries_for_point += 1
+#        done = False
+#        if error_test(self.robot.p_e, self.goal):
+#            info = {
+#                'is_success': np.float32(1.0),
+#            }
+#        else:
+#            info = {
+#                'is_success': np.float32(0.0),
+#            }
+#        return done
+
+
+# NOTE: BROKEN AS IT'S FOR ONLY ONE ROBOT
+    def step(self, action):
+        self.robot.forwardKinmViaPositions(action[:-1] / self.damping, action[-1])
+        self.n_of_tries_for_point += 1
+        obs = self._get_obs()
+
+        done = False
+        if error_test(self.robot.p_e, self.goal):
+            info = {
+                'is_success': np.float32(1.0),
+            }
+        else:
+            info = {
+                'is_success': np.float32(0.0),
+            }
+        reward = self.compute_reward(obs['achieved_goal'], self.goal, info)
+        self.episode_score += reward
+        return obs, reward, done, info
+
+
+
+
+    def reset(self):
+        # TODO: initialize robot joints state to a random (but valid (in joint range)) initial state
+        #       when using joint clamping
+        self.episode_score = 0
+        self.n_of_points_done += 1
+        self.n_of_tries_for_point = 0
+        self.data = []
+
+        # generate new point
+        # NOTE: simply taken away 'cos now it's set with a slider
+        #self.goal = np.array([random.uniform(-0.70, 0.70), random.uniform(-0.70, 0.70), random.uniform(-0.70, 0.70)])
+        
+        # initialize to a random starting state and check whether it makes any sense
+        thetas = []
+        for joint in self.robot.joints:
+             thetas.append(6.28 * np.random.random() - 3.14)
+        self.robot.forwardKinmViaPositions(thetas, 1)
+
+        if self.chill_reset == True:
+            sensibility_check = False
+            while not sensibility_check:
+                thetas = []
+                for joint in self.robot.joints:
+                     thetas.append(6.28 * np.random.random() - 3.14)
+                self.robot.forwardKinmViaPositions(thetas , 1)
+                if calculateManipulabilityIndex(self.robot) > 0.15:
+                    sensibility_check = True
+        
+#        for i, joint in enumerate(self.robots[robot_index].joints):
+#            joint.theta = self.robots[0].joints[i].theta
+
+# NOTE: not needed and i'm not fixing _get_obs for more robots
+        obs = self._get_obs()
+        return obs
+
+    def render(self, mode='human', width=500, height=500):
+        try:
+            self.drawingInited == False
+        except AttributeError:
+            self.drawingInited = False
+
+        if self.drawingInited == False:
+            #plt.ion()
+            self.fig = plt.figure()
+            #self.ax = self.fig.add_subplot(111, projection='3d')
+            self.ax = self.fig.add_subplot(111, projection='3d')
+            # these are for axes scaling which does not happen automatically
+            self.ax.plot(np.array([0]), np.array([0]), np.array([1.5]), c='b')
+            self.ax.plot(np.array([0]), np.array([0]), np.array([-1.5]), c='b')
+            plt.xlim([-1.5,1.5])
+            plt.ylim([-0.5,1.5])
+            color_link = 'black'
+            self.robot.initDrawing(self.ax, color_link)
+            self.drawingInited = True
+            plt.pause(0.1)
+            # put this here for good measure
+            self.robot.drawStateAnim()
+            self.bg = self.fig.canvas.copy_from_bbox(self.fig.bbox)
+        # manual blitting basically
+        # restore region
+        self.fig.canvas.restore_region(self.bg)
+        # updating all the artists (set new properties (position bla))
+        self.robot.drawStateAnim()
+        # now you need to manually draw all the artist (otherwise you update everything)
+        # thank god that's just lines, all in a list.
+        # btw, that should definitely be a dictionary, not a list,
+        # this makes the code fully unreadable (although correct).
+        for link in self.robot.lines:
+            for line in link:
+                self.ax.draw_artist(line)
+        self.fig.canvas.blit(self.fig.bbox)
+        # NOTE: this might not work
+        self.ax.set_title(str(self.n_of_tries_for_point) + 'th iteration toward goal')
+        drawPoint(self.ax, self.goal, 'red', 'o')
+        # if no draw it is kinda faster
+        #self.fig.canvas.draw()
+        # this is even faster
+        #self.fig.canvas.update()
+        self.fig.canvas.flush_events()
+
+
+    def reset_test(self):
+        self.episode_score = 0
+        self.n_of_points_done += 1
+        self.n_of_tries_for_point = 0
+
+        # generate new point
+        self.goal = np.array([random.uniform(-0.70, 0.70), random.uniform(-0.70, 0.70), random.uniform(-0.70, 0.70)])
+        
+        # DO NOT initialize to a random starting state, keep the previous one
+
+        obs = self._get_obs()
+        return obs
+
+
+    def close(self):
+        # close open files if any are there
+        pass
+
+
+    # various uitility functions COPIED from fetch_env
+
+    def seed(self, seed=None):
+        self.np_random, seed = seeding.np_random(seed)
+        return [seed]
+
+
+    def _get_obs(self):
+        thetas = []
+        for joint in self.robot.joints:
+            thetas.append(joint.theta)
+        thetas = np.array(thetas , dtype=np.float32)
+        obs = self.robot.p_e.copy()
+        obs = np.append(obs, thetas)
+
+        return {
+            'observation': obs,
+            'achieved_goal': self.robot.p_e.copy(),
+            'desired_goal': self.goal.copy(),
+        }
+
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/InverseKinematics.cpython-310.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/InverseKinematics.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c59cab0f7519bb21e35ad76ebfa41a84b0376333
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/InverseKinematics.cpython-310.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/InverseKinematics.cpython-311.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/InverseKinematics.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e19567ae0945d05a0ef9d5b7593f2eaf8ca7e1b2
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/InverseKinematics.cpython-311.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/drawing.cpython-310.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/drawing.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c4ec066dd49fb82dc8ca4b1a060b56e912243739
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/drawing.cpython-310.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/drawing.cpython-311.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/drawing.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..60bcda8221c70f3842b39da90028011be8e56d07
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/drawing.cpython-311.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/drawing_for_anim.cpython-310.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/drawing_for_anim.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..edfb210d0a6e98b4d5e63a27394a09f5931708f4
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/drawing_for_anim.cpython-310.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/drawing_for_anim.cpython-311.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/drawing_for_anim.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b39611c8807a0497f50ad6faa2f76392b6793fb9
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/drawing_for_anim.cpython-311.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/follow_curve.cpython-310.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/follow_curve.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..82f14ea95893dce69a57d1c4cc66d153d88e93c2
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/follow_curve.cpython-310.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/follow_curve.cpython-311.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/follow_curve.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..8bf02d5aa7da680701e2164359fa19bae13fb63f
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/follow_curve.cpython-311.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/forw_kinm.cpython-310.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/forw_kinm.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..8d2d9b38d7d0ce710da387e407cfcc3cab8e0f3f
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/forw_kinm.cpython-310.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/forw_kinm.cpython-311.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/forw_kinm.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ec97b7a1cca924c836514e165d58713cb9915c19
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/forw_kinm.cpython-311.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/inv_kinm.cpython-310.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/inv_kinm.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..019ec0fc3322ff2667d95fd3933437d829cd2882
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/inv_kinm.cpython-310.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/inv_kinm.cpython-311.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/inv_kinm.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..523e8c16464f8c63490ac3fca3f2ef086e483ae1
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/inv_kinm.cpython-311.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/joint_as_hom_mat.cpython-310.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/joint_as_hom_mat.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..5ba205c8b7b413f27c4deb1fe4e9770e874c7c6e
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/joint_as_hom_mat.cpython-310.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/joint_as_hom_mat.cpython-311.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/joint_as_hom_mat.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..936a5cf11707e3e29e66b708b2fad513c0bec527
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/joint_as_hom_mat.cpython-311.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/utils.cpython-310.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/utils.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4c46e6f66c03ec78ccfa78f5c4ec2a045d0df4e9
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/utils.cpython-310.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/utils.cpython-311.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/utils.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ac5e6c96318a30402f6390f79b59f8d9d753523f
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/utils.cpython-311.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/webots_api_helper_funs.cpython-310.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/webots_api_helper_funs.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d0a69e62d20ea45aceaf28eb99b3149a77c7d3f3
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/webots_api_helper_funs.cpython-310.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/__pycache__/webots_api_helper_funs.cpython-311.pyc b/python/ur_simple_control/visualize/robot_stuff/__pycache__/webots_api_helper_funs.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..fd4d62b162bb90fbf4b4b0b69abfcdf75bcc9425
Binary files /dev/null and b/python/ur_simple_control/visualize/robot_stuff/__pycache__/webots_api_helper_funs.cpython-311.pyc differ
diff --git a/python/ur_simple_control/visualize/robot_stuff/drawing.py b/python/ur_simple_control/visualize/robot_stuff/drawing.py
new file mode 100644
index 0000000000000000000000000000000000000000..d2c85e2ea353c7e0a14bce8ea1e7077dab0adf57
--- /dev/null
+++ b/python/ur_simple_control/visualize/robot_stuff/drawing.py
@@ -0,0 +1,36 @@
+import numpy as np
+import matplotlib.pyplot as plt 
+import mpl_toolkits.mplot3d.axes3d as p3
+from matplotlib.animation import FuncAnimation
+import matplotlib.colors as colr
+
+
+def drawOrientation(ax, orientation, t_v, avg_link_lenth):
+    # every line is drawn be specifing its x, y and z coordinates
+    # thus we will take each relevant vector and turn every one of its coordinates into a steps array
+    # and every vector will be translated accordingly
+    # first we need to take rot mat and p out of hom mat
+    steps = np.arange(0.0, 1.0, 0.1) * (avg_link_lenth / 2)
+    
+    # now we draw the orientation of the current frame
+    col = ['b', 'g', 'r']
+    for i in range(0,3):
+        x = t_v[0] + orientation[i,0] * steps
+        y = t_v[1] + orientation[i,1] * steps
+        z = t_v[2] + orientation[i,2] * steps
+        ax.plot(x, y, z, color=col[i])
+
+
+def drawVector(ax, link, t_v, color_link):
+    # first let's draw the translation vector to the next frame
+    steps = np.arange(0.0, 1.0, 0.1)
+    x = t_v[0] + link[0] * steps
+    y = t_v[1] + link[1] * steps
+    z = t_v[2] + link[2] * steps
+    ax.plot(x, y, z, color=color_link)
+
+
+
+def drawPoint(ax, p, color_inside, marker):    
+    point, = ax.plot([p[0]], [p[1]], [p[2]], markerfacecolor=color_inside, markeredgecolor=color_inside, marker=marker, markersize=5.5, alpha=0.9)
+    return point
diff --git a/python/ur_simple_control/visualize/robot_stuff/drawing_for_anim.py b/python/ur_simple_control/visualize/robot_stuff/drawing_for_anim.py
new file mode 100644
index 0000000000000000000000000000000000000000..f40ad14a8ba80cdcbc5a4e424cf3878671c770a9
--- /dev/null
+++ b/python/ur_simple_control/visualize/robot_stuff/drawing_for_anim.py
@@ -0,0 +1,38 @@
+import numpy as np
+import matplotlib.pyplot as plt 
+import mpl_toolkits.mplot3d.axes3d as p3
+from matplotlib.animation import FuncAnimation
+import matplotlib.colors as colr
+
+
+def drawOrientationAnim(ax, orientation, orientation_lines, t_v, avg_link_lenth):
+    # every line is drawn be specifing its x, y and z coordinates
+    # thus we will take each relevant vector and turn every one of its coordinates into a steps array
+    # and every vector will be translated accordingly
+    # first we need to take rot mat and p out of hom mat
+    steps = np.arange(0.0, 1.0, 0.1) * (avg_link_lenth / 2)
+    
+    # now we draw the orientation of the current frame
+    col = ['b', 'g', 'r']
+    for i in range(0,3):
+        x = t_v[0] + orientation[i,0] * steps
+        y = t_v[1] + orientation[i,1] * steps
+        z = t_v[2] + orientation[i,2] * steps
+#        ax.plot(x, y, z, color=col[i])
+
+        orientation_lines[i].set_data(x, y)
+        orientation_lines[i].set_3d_properties(z)
+
+def drawVectorAnim(ax, link, link_line, t_v, color_link):
+    # first let's draw the translation vector to the next frame
+    steps = np.arange(0.0, 1.0, 0.1)
+    x = t_v[0] + link[0] * steps
+    y = t_v[1] + link[1] * steps
+    z = t_v[2] + link[2] * steps
+    #ax.plot(x, y, z, color=color_link)
+    link_line.set_data(x, y)
+    link_line.set_3d_properties(z)
+
+
+
+
diff --git a/python/ur_simple_control/visualize/robot_stuff/follow_curve.py b/python/ur_simple_control/visualize/robot_stuff/follow_curve.py
new file mode 100644
index 0000000000000000000000000000000000000000..49feb200e0b6e71e98551ddb5307fcee46c7a95d
--- /dev/null
+++ b/python/ur_simple_control/visualize/robot_stuff/follow_curve.py
@@ -0,0 +1,102 @@
+"""control_test controller."""
+
+from robot_stuff.forw_kinm import *
+from robot_stuff.inv_kinm import *
+#from anim_func import *
+import numpy as np
+#import matplotlib.pyplot as plt 
+#import mpl_toolkits.mplot3d.axes3d as p3
+#from matplotlib.animation import FuncAnimation
+#import matplotlib.colors as colr
+import sys
+import subprocess
+import scipy.optimize
+import random
+
+
+
+# angle in radians ofc
+def turnAngleToPlusMinusPiRange(angle):
+    return np.arcsin(np.sin(angle))
+# let's trace out a circle
+# th circle formula is:
+# x = radius * cos(t)
+# y = radius * sin(t)
+# z = height
+# the direction you are to move in in order to trace a circle,
+# or any parametric curve for that matter is
+# in the direction of the derivative at the given point
+# the derivative w.r.t. t is, by coordinate:
+# x = radius * -1 * sin(t)
+# y = radius * cos(t)
+# z = 0
+def goInACirleViaDerivative(radius, height, current_parameter):
+    return np.array([radius * -1 * np.sin(curve_parameter), radius * np.cos(curve_parameter), 0], dtype=float32)
+
+
+# or just pass the next point to reach and use the vector to get there lel
+def goInACirleViaPositionAroundZ(radius, height, current_parameter):
+    return np.array([radius * np.cos(curve_parameter), radius * np.sin(curve_parameter), height], dtype=np.float32)
+
+# here height is distance from yz plane
+# but also from xy plane (we want it to be floating around axis parallel to x axis actually)
+def goInACirleViaPositionAroundLiftedX(radius, height, x_0, current_parameter):
+    return np.array([x_0, radius * np.sin(curve_parameter), height
+                + radius * np.cos(curve_parameter)], dtype=np.float32)
+
+
+def error_test(robot, t):
+    e = abs(t - r.p_e)
+    if e[0] < 0.001 and e[1] < 0.001 and e[2] < 0.001:
+        return True
+    else:
+        return False
+
+# let radius be 0.4
+def goInACircleViaJacobian(circle_param):
+    """ doing a fixed circle with axis around x axis because
+    i only need one circle and this is easy
+    arg: s - the current parameter in the parametric eq."""
+    # R0c is the rotational matrix for the circle
+    # p is the vector to the circle center
+
+    # need to have a hom. transf. mat. to circle center
+    # first 3 entries are rot. mat. and last one is translation vec.
+    # with added hom. mat. padding
+    circ_hom_mat = np.array([[0.0, 0.0, -1.0, 0.0], \
+                            [0.0, -1.0, 0.0, 0.0],
+                            [1.0, 0.0, 0.0, 0.0],
+                            [-0.59, 0.0, 0.59, 1.0]], dtype=np.float32)
+    center = circ_hom_mat[0:3, 3]
+    R0c = circ_hom_mat[0:3, 0:3]
+
+    # shouldn't jacobian be a 3x3 mat? 
+    circle_jacobian_circle = np.array([-1 * np.sin(circle_param), np.cos(circle_param), 0], dtype=np.float32)
+    circle_jacobian_base = R0c @ circle_jacobian_circle
+
+    # now we need a radij-vector
+    # you need circle center coordinates and some point on the circle
+    # i can calculate p from current parameter value 
+    # i do that in circle's frame
+    # and then i can transport it to base frame with my hom. transform
+
+    point_on_c_circle = np.array([np.cos(circle_param), np.sin(circle_param), 0.0], dtype=np.float32)
+    point_on_c_circle = np.hstack((point_on_c_circle, 1))
+    point_on_c_base = circ_hom_mat @ point_on_c_circle
+
+    # need to make center hom.
+    center = np.hstack((center, 0))
+    radij_vec = point_on_c_base - center
+
+    radius = 0.4
+
+    s =  radius * np.arctan2(radij_vec[1], radij_vec[0])
+
+    s = turnAngleToPlusMinusPiRange(s) + np.pi - s 
+    es = s * 2 * np.pi * radius - s
+
+    e = es * circle_jacobian_base
+    return e
+    
+
+
diff --git a/python/ur_simple_control/visualize/robot_stuff/forw_kinm.py b/python/ur_simple_control/visualize/robot_stuff/forw_kinm.py
new file mode 100644
index 0000000000000000000000000000000000000000..0a54f807779e36f9c913ea72e29e149c5dfd516e
--- /dev/null
+++ b/python/ur_simple_control/visualize/robot_stuff/forw_kinm.py
@@ -0,0 +1,544 @@
+import numpy as np
+#import matplotlib.pyplot as plt 
+#import mpl_toolkits.mplot3d.axes3d as p3
+#from matplotlib.animation import FuncAnimation
+#import matplotlib.colors as colr
+from robot_stuff.webots_api_helper_funs import *
+import scipy.linalg
+
+#from drawing import *
+from robot_stuff.joint_as_hom_mat import *
+from robot_stuff.drawing import *
+from robot_stuff.drawing_for_anim import *
+
+"""
+The Robot_raw class is a collection of joints which
+together constitute the manipulator. 
+It has methods for calculating the Jacobian,
+the velocity manipulability Jacobian and two
+different methods for calculating a gradient
+toward a better manipulability positions.
+
+The Jacobian is calculated from its geometric
+derivation as described in Siciliano chapter 3,
+and so is the velocity manipulability Jacobian,
+as described in Geometry Aware Manipulability 
+Learning, Tracking and Transfer or in 
+Symbolic differentiation of the velocity mapping for a serial kinematic chain,
+by Bruyninckx. Here i avoided the tensor 
+formulation as to my knowledge
+numpy does not support tensor operations.
+
+
+Calculations of the gradients are described in the 
+Singularity Avoidance Using Riemannian Geometry paper.
+
+
+notes on code:
+- we are doing only positions, not orientations
+- dh parameters are read from a file
+- there is no other way to import a robot
+
+
+"""
+# TODO 
+# TODO 
+# TODO 
+# TODO add the following flags:
+# one for (not) drawing in matplotlib
+# one for (not) using Webots API
+
+
+
+# add a drawing flag so that you can turn it on and off
+# if sim is to be had, you must pass motors=[list_of_motors] 
+# and sensors=[list_of_sensors] 
+class Robot_raw:
+    #def __init__(self, clamp):
+    def __init__(self, ax=None, **kwargs):
+        try:
+            self.motors = kwargs["motors"]
+            self.sensors = kwargs["sensors"]
+            self.robot_name = kwargs["robot_name"]
+            self.sim = 1
+        except KeyError:
+            if len(kwargs) > 1:
+                print(" if sim is to be had, you must pass \n \
+                        motors=[list_of_motors] \n \
+                         and sensors=[list_of_sensors] in named args syntax")
+                exit(1)
+            else:
+                self.sim = 0
+                self.robot_name = "no_sim"
+            pass
+        #self.mode = 'training'
+        self.mode = 'eval'
+        self.clamp = 0
+        #self.clamp = 1
+        self.joints = []
+        # TODO FIX READING FILES 
+        if self.robot_name == "no_sim":
+            #fil = open('inverse_kinematics_gymified/envs/arms/ur10e_dh_parameters_from_the_ur_site', 'r')
+            #fil = open('envs/arms/ur10e_dh_parameters_from_the_ur_site', 'r')
+            #fil = open('arms/ur10e_dh_parameters_from_the_ur_site', 'r')
+            #fil = open('/chalmers/users/guberina/ActorCriticManipulationControl/inverse_kinematics_gymified/inverse_kinematics_gymified/envs/arms/ur10e_dh_parameters_from_the_ur_site', 'r')
+            #fil = open('./arms/ur10e_dh_parameters_from_the_ur_site', 'r')
+            fil = open('./arms/ur5e_dh', 'r')
+            #fil = open('/home/gospodar/chalmers/ADL/ActorCriticManipulationControl/inverse_kinematics_gymified/inverse_kinematics_gymified/envs/arms/kuka_lbw_iiwa_dh_params', 'r')
+            print("i'm using: testing_dh_parameters (which are UR10e params)")
+        if self.robot_name == "UR10e":
+            fil = open('arms/ur10e_dh_parameters_from_the_ur_site', 'r')
+            print('im using: ur10e_dh_parameters_from_the_ur_site')
+        if self.robot_name == "base_link":
+            fil = open('arms/kuka_lbw_iiwa_dh_params', 'r')
+#            fil = open('./da   /lbr_iiwa_jos_jedan_dh', 'r')
+            print("i'm using: kuka_lbw_iiwa_dh_params")
+        if self.robot_name == "j2n6s300":
+            fil = open('arms/j2n6s300_dh_params', 'r')
+            print("i'm using: j2n6s300_dh_params")
+        if self.robot_name == "j2n6s300":
+            fil = open('arms/j2n6s300_dh_params', 'r')
+            print("i'm using: j2n6s300_dh_params")
+        if self.robot_name == "j2s7s300_link_base":
+            fil = open('arms/j2s7s300_dh_params', 'r')
+            print("i'm using: j2n6s300_dh_params")
+
+        params = fil.read().split('\n')
+        params.pop()
+        for p in params:
+            p = p.split(';')
+            self.joints.append(Joint(float(p[0]), float(p[1]), float(p[2]), float(p[3]), self.clamp))
+
+        self.ndof = len(self.joints)
+
+        fil.close()
+        self.jacobian = 0
+        self.calcJacobian()
+
+
+# below are some utility functions for drawing
+    def initDrawing(self, ax, color_link):
+        #initialize the plot for each line in order to animate 'em
+        # each joint equates to four lines: 3 for orientation and 1 for the link
+        # and each line has its x,y and z coordinate
+        # thus 1 joint is: [x_hat, y_hat, z_hat, p] 
+        # and lines are a list of all of those
+        self.ax = ax
+        self.color_link = color_link
+        self.lines = []
+        avg_link_lenth = 0
+        for j in range(self.ndof):
+            x_hat = self.joints[j].HomMat[0:3,0]
+            y_hat = self.joints[j].HomMat[0:3,1]
+            z_hat = self.joints[j].HomMat[0:3,2]
+            p = self.joints[j].HomMat[0:3,3]
+
+            #line_x, = self.ax.plot(np.array([]),np.array([]),np.array([]), 'r')#, animated=True) 
+            #line_y, = self.ax.plot(np.array([]),np.array([]),np.array([]), 'g')#, animated=True)
+            #line_z, = self.ax.plot(np.array([]),np.array([]),np.array([]), 'b')#, animated=True)
+            #line_p, = self.ax.plot(np.array([]),np.array([]),np.array([]), self.color_link)#, animated=True)
+            line_x, = self.ax.plot(np.array([]),np.array([]),np.array([]), 'r', animated=True) 
+            line_y, = self.ax.plot(np.array([]),np.array([]),np.array([]), 'g', animated=True)
+            line_z, = self.ax.plot(np.array([]),np.array([]),np.array([]), 'b', animated=True)
+            line_p, = self.ax.plot(np.array([]),np.array([]),np.array([]), self.color_link, animated=True)
+
+            self.lines += [[line_x, line_y, line_z, line_p]]
+            avg_link_lenth += self.joints[j].d + self.joints[j].r
+
+        self.avg_link_lenth = avg_link_lenth / self.ndof
+
+
+# NOTE switched from ax as argument to self.ax
+    def drawStateAnim(self):
+        toBase = [np.array([[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,1]])]
+        drawOrientation(self.ax, toBase[-1][0:3,0:3], np.zeros((3,)), self.avg_link_lenth * 2)
+        for j in range(len(self.joints)):
+            toBase.append(toBase[-1] @ self.joints[j].HomMat)
+            drawOrientationAnim(self.ax, toBase[-1][0:3,0:3], self.lines[j][:-1], toBase[-1][0:3,3], self.avg_link_lenth)
+            drawVectorAnim(self.ax, -1* ( toBase[-2][0:3,3] - toBase[-1][0:3,3] ), self.lines[j][-1], toBase[-2][0:3,3],self.color_link)
+
+
+# needed only for drawing, and even that is optional 
+    def saveConfToFile(self):
+        fil = open('robot_parameters', 'r')
+        params = fil.read().split('\n')
+        fil.close()
+        fil = open('robot_parameters', 'w')
+        params.pop()
+        back_to_string = ''
+        for i in range(len(params)):
+            params[i] = params[i].split(';')
+            params[i][1] = self.joints[i].theta
+            for j in range(4): #n of d-h params
+                back_to_string += str(params[i][j])
+                if j != 3:
+                    back_to_string += ';'
+                else:
+                    back_to_string += '\n'
+        fil.write(back_to_string) 
+        fil.close()
+
+
+# the jacobian is calulated for the end effector in base frame coordinates
+#   TODO: the base frame ain't aligned with world coordinate frame!!!
+#       ==> the only offset is on the z axis tho
+#       ==> resolved by putting first joint's d parameter to height, there is no x nor y offset
+# the simulation is controlled via motors
+# we do calculations by reading from the sensors 
+# and we send the results to the motors
+
+# HERE WE ALSO CALCULATE THE MANIPULABILITY JACOBIAN BUT I WON'T RENAME
+    def calcJacobian(self):
+        z_is = [np.array([0,0,1], dtype=np.float32)]
+        p_is = [np.array([0,0,0], dtype=np.float32)]
+        toBase = np.array([[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,1]], dtype=np.float32)
+        self.p_e = np.array([0.,0.,0.], dtype=np.float32)
+        
+        for j in range(self.ndof):
+            toBase = toBase @ self.joints[j].HomMat
+            z_is.append(toBase[0:3, 2])
+            p_is.append(toBase[0:3, 3])
+        p_e = p_is[-1]
+        self.p_e = p_is[-1]
+        jac = np.array([0,0,0,0,0,1], dtype=np.float32).reshape(6,1)
+# we doin only revolute joints still 
+# we'll put these into array cos i don't trust myself that much
+        j_os = []
+        j_ps = []
+
+        for j in range(self.ndof):
+            j_p = np.cross(z_is[j], p_e - p_is[j])
+            j_ps.append(j_p)
+            j_p = j_p.reshape(3,1)
+            j_o = z_is[j]
+            j_os.append(z_is[j])
+            j_o = j_o.reshape(3,1)
+            j_i = np.vstack((j_p, j_o))
+            jac = np.hstack((jac, j_i))
+        self.jacobian = jac[0:6,1:]
+#        print("jacobian incoming")
+#        print(self.jacobian)
+        self.jac_tri = self.jacobian[0:3,:]
+
+    # the manipulability elipsoid is J @ J.T
+    # and we want to know its derivative w.r.t. q (all joints)
+    # thus we calculate the derivative of a matrix w.r.t a vector
+    # the result is a 3rd order tensor ('cos you get 1 Jacobian for each q_i)
+    # with size 6 x n x n
+    # the calculations are described in Geometry-aware learning, tracking.., eq. 66-68
+    # just figure out the shape of the tensor and it's all clear after that
+    # formulae straight from GAMLTT, last page, 66-68
+
+#        if self.mode == 'eval':
+        mjac = []
+        mjac_tri = []
+        # this first column is here so that we can stack in the for loop, it's removed later
+        mjac_j = np.array([0,0,0,0,0,1], dtype=np.float32).reshape(6,1)
+        mj_o = 0
+        mj_p = 0
+        # now we take a jth joint by which we will do the derivate
+        for j in range(self.ndof):
+            # and we do this for every ith joint
+            for i in range(self.ndof):
+                if j <= i:
+                    mj_o = np.cross(j_os[j], j_os[i]).reshape(3,1)
+                    mj_p = np.cross(j_os[j], j_ps[i]).reshape(3,1)
+                    mj_i = np.vstack((mj_p, mj_o))
+                    mjac_j = np.hstack((mjac_j, mj_i))
+
+                else:
+                    mj_o = np.array([0.,0.,0.], dtype=np.float32).reshape(3,1)
+                    mj_p = np.cross(j_ps[j], j_os[i]).reshape(3,1)
+                    mj_i = np.vstack((mj_p, mj_o))
+                    mj_i = -1 * mj_i
+                    mjac_j = np.hstack((mjac_j, mj_i))
+
+            # with that we calculated J derivative w.r.t. joint j, so append that and move on
+            mjac_j = mjac_j[0:6,1:]
+#            print("mjac_j", j + 1)
+#            print(mjac_j)
+            mjac_j_tri = mjac_j[0:3,:]
+            mjac.append(mjac_j)
+#            print("unutar calcJac")
+#            print(mjac_j)
+#            print("unutar calcJac")
+            mjac_tri.append(mjac_j_tri)
+            mjac_j = np.array([0,0,0,0,0,1], dtype=np.float32).reshape(6,1)
+        self.mjac = mjac
+        self.mjac_tri = mjac_tri
+
+# now we that we have the manipulability jacobian,
+# we can get the velocity manipulability jacobian
+    
+
+    # implementation of maric formula 12 (rightmostpart)
+    def calcMToEGradient_kM(self):
+        # first let's calculate the manipulability elipsoid
+        M = self.jacobian @ self.jacobian.T
+        M = M[0:3, 0:3]
+        k = np.trace(M)
+#        k = 2
+#        print(k)
+        k_log = np.log(k)
+
+        # this is the derivative of the manipulability jacobian w.r.t. joint angles
+   #     self.calcManipulabilityJacobian()
+
+        # we need M^-1 which might not exist in which case we'll return a 0 
+        # needs to throw an error or something along those lines in production tho
+        try:
+            M_inv = np.linalg.inv(M)
+        except np.linalg.LinAlgError as e:
+            print("ROKNUH U SINGULARITET!!!!!!!!!!!")
+#            M_inv = np.eye(M.shape[1], M.shape[0]) 
+            return np.array([0] * self.ndof)
+
+
+        # now we right-mul M_inv by each partial derivative and do a trace of that
+        resulting_coefs = []
+        for i in range(self.ndof):
+            # we first need to calculate an appropriate element of the
+            # velocity manipulability ellipsoid
+            # J^x_i = mjac[i] @ J.T + J @ mjac[i].T
+            #M_der_by_q_i = self.mjac_tri[i] @ self.jac_tri.T + self.jac_tri @ self.mjac_tri[i].T
+            M_der_by_q_i = self.mjac[i] @ self.jacobian.T + self.jacobian @ self.mjac[i].T
+            M_der_by_q_i = M_der_by_q_i[0:3, 0:3]
+            resulting_coefs.append(-2 * k_log * np.trace(M_der_by_q_i @ M_inv))
+        resulting_coefs = np.array(resulting_coefs)
+#        print(resulting_coefs)
+        return resulting_coefs
+
+       
+    # let's actually strech toward the sphere sigma = kI
+    def calcMToEGradient_kI(self):
+        # first let's calculate the manipulability elipsoid
+        M = self.jacobian @ self.jacobian.T
+        M = M[0:3, 0:3]
+        k = np.trace(M)
+        sigma = k * np.eye(3)
+#        sigma = 10 * k * np.eye(3)
+        sigma_sqrt = scipy.linalg.fractional_matrix_power(sigma, -0.5)
+        Theta = sigma_sqrt @ M @ sigma_sqrt
+
+        log_Theta = scipy.linalg.logm(Theta)
+        Theta_der_wrt_q_i = []
+        # calc the M derivate wrt q_is and same for Theta 
+        for i in range(self.ndof):
+            M_der_by_q_i = self.mjac[i] @ self.jacobian.T + self.jacobian @ self.mjac[i].T
+            M_der_by_q_i = M_der_by_q_i[0:3, 0:3]
+            Theta_der_wrt_q_i.append(sigma_sqrt @ M_der_by_q_i @ sigma_sqrt)
+        # now this is the E
+        E = np.array(Theta_der_wrt_q_i)
+
+        # to compute the derivative we have to use the frechet derivative
+        # on the [[Theta, E], [0, Theta]]
+#        gradMtoE = []
+        resulting_coefs = []
+        for i in range(self.ndof):
+            mat_for_frechet = np.vstack((np.hstack((Theta, E[i])), \
+                np.hstack((np.eye(3) - np.eye(3), Theta))))
+            frechet_der = scipy.linalg.logm(mat_for_frechet)
+            der_theta_q_i = frechet_der[0:3, -3:]
+            resulting_coefs.append(2 * np.trace(der_theta_q_i @ log_Theta.T))
+
+        return np.array(resulting_coefs)
+
+
+    def calcManipMaxGrad(self):
+        M = self.jacobian @ self.jacobian.T
+        M = M[0:3, 0:3]
+        resulting_coefs = []
+        print("pinv")
+        print(np.linalg.pinv(self.jacobian))
+        for i in range(self.ndof):
+            A = self.mjac[i] @ np.linalg.pinv(self.jacobian)
+#            print("A", i)
+#            print(A)
+#            A = A[0:3, 0:3]
+            resulting_coefs.append(-1 * np.sqrt(np.linalg.det(M)) * np.trace(A))
+
+        return np.array(resulting_coefs)
+
+
+
+            
+
+    def drawState(self):
+        toBase = [np.array([[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,1]])]
+        drawOrientation(self.ax, toBase[-1][0:3,0:3], np.zeros((3,)))
+        for j in range(self.ndof):
+            toBase.append(toBase[-1] @ self.joints[j].HomMat)
+            drawOrientation(self.ax, toBase[-1][0:3,0:3], toBase[-1][0:3,3])
+            drawVector(self.ax, -1* ( toBase[-2][0:3,3] - toBase[-1][0:3,3] ), toBase[-2][0:3,3],self.color_link)
+
+
+    def updateJointsAndJacobian(self):
+        thetas = np.array(readJointState(self.sensors, dtype=np.float32))
+# potential clamping for joint rotation limits
+# offset for j2n6s300
+        if self.robot_name == "j2n6s300":
+            q1 = np.array([-0.01497, 1.5708, -1.5708, -1.5708, -3.14159, 0.0], dtype=np.float32)
+            thetas = thetas + q1
+#        if self.robot_name == "base_link":
+#            thetas = thetas + np.array([np.pi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], dtype=np.float32) 
+        for i in range(len(thetas)):
+            if self.clamp == 1:
+                self.joints[i].rotate_numerically(clampTheta(thetas[i]), self.clamp)
+            else:
+                self.joints[i].rotate_numerically(thetas[i], self.clamp)
+        self.calcJacobian()
+
+    # only here for plots, hence does not update jacobian
+    def setJoints(self, thetas):
+        for i in range(len(thetas)):
+            if self.clamp == 1:
+                self.joints[i].rotate_numerically(clampTheta(thetas[i]), self.clamp)
+            else:
+                self.joints[i].rotate_numerically(thetas[i], self.clamp)
+
+
+
+    def forwardKinmViaPositions(self, thetas, extra_damping):
+# potential clamping for joint rotation limits
+#   ==> ur10e does not have joint limits, but other robots might
+    # offset for j2n6s300
+        if self.robot_name == "j2n6s300":
+            thetas = thetas + np.array([np.pi,  np.pi,  np.pi,  \
+                    np.pi,  0.0,  1.57079633], dtype=np.float32)
+#        if self.robot_name == "base_link":
+#            thetas = thetas + np.array([np.pi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) 
+        for i in range(len(thetas)):
+            # clamp
+            if self.clamp == 1:
+                if self.sim == 1:
+                    self.motors[i].setPosition(clampTheta(self.joints[i].theta + thetas[i]))
+                else:
+                    self.joints[i].rotate_numerically(clampTheta(self.joints[i].theta + thetas[i] / extra_damping), self.clamp)
+            # no clamp
+            else:
+                if self.sim == 1:
+                    self.motors[i].setPosition(self.joints[i].theta + thetas[i])
+                else:
+                    self.joints[i].rotate_numerically(thetas[i] + self.joints[i].theta / extra_damping, self.clamp)
+
+        if self.sim == 1:
+            self.updateJointsAndJacobian()
+        else:
+            if self.mode == "eval":
+                self.calcJacobian()
+
+
+
+    def forwardKinmNumericsOnlyDebug(self, thetas):
+#        if self.robot_name == "j2n6s300":
+#            thetas = thetas + np.array([np.pi,  np.pi, np.pi, np.pi, 0.0, np.pi]) 
+        for i in range(len(thetas)):
+            # clamp
+            if self.clamp == 1:
+                if self.sim == 1:
+                    self.motors[i].setPosition(clampTheta(thetas[i]))
+#                    self.updateJointsAndJacobian()
+                else:
+                    self.joints[i].rotate_numerically(clampTheta( thetas[i]), self.clamp)
+                    self.calcJacobian()
+            # no clamp
+            else:
+                if self.sim == 1:
+#                    self.motors[i].setPosition(thetas[i])
+                    self.updateJointsAndJacobian()
+                else:
+                    self.joints[i].rotate_numerically(thetas[i] , self.clamp)
+                    self.calcJacobian()
+
+
+    def forwardKinmNumericsOnlyDebug2(self, thetas):
+        print("======================================")
+#        if self.robot_name == "j2n6s300":
+#            thetas = thetas + np.array([np.pi,  np.pi /2,  -np.pi/2,  \
+#                    0.0,  0.0,  1.57079633])
+        for i in range(self.ndof):
+            if self.clamp == 1:
+                self.joints[i].rotate_numerically(clampTheta(thetas[i]), self.clamp)
+            else:
+        #        print("----------- +", thetas[i])
+                self.joints[i].rotate_numerically(thetas[i], self.clamp)
+        #        print("after:", self.joints[i].theta)
+        #        print("")
+        print("")
+        print("")
+        print("")
+        self.calcJacobian()
+#        print(self.jacobian)
+#        print(self.mjac)
+
+
+
+# in webots, i get this for free with a position sensor
+# but since such a device is not available in reality.
+# thus, the gps device in webots shall be used to measure accuracy.
+    def eePositionAfterForwKinm(self, thetas):
+        joints2 = self.joints
+        for i in range(len(thetas)):
+            joints2[i].rotate(joints2[i].theta + thetas[i])
+
+        toBase = np.array([[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,1]], dtype=np.float32)
+        
+        for j in range(len(joints2)):
+            toBase = toBase @ joints2[j].HomMat
+        p_e = toBase[0:3,3]
+        return p_e
+        
+    
+    def bruteAnimForwKinm(self, ax, thetas):
+        shots = np.linspace(0,1,20)
+        for shot in shots:
+            for i in range(len(thetas)):
+                self.joints[i].rotate(self.joints[i].theta + thetas[i] * shot)
+            self.drawState(ax, 'bisque')
+        self.calcJacobian()
+        self.drawState(ax, 'b')
+
+    
+    # remake this into the FuncAnimation update function
+    # divide each angle from the current to desired angle in the same number of steps
+    # then for each joint and each of its steps calculate the new position,
+    # and append it in the appropriate "line" list
+# there is no set_data() for 3dim, so we use set_3_properties() for the z axis
+def forwardKinAnim(frame, r, thetas, n_of_frames, ax):
+    thetas_sliced = []
+#    print("frame")
+ #   print(frame)
+    for j in range(len(r.joints)):
+        # i have no idea why but the sum of frame(i)/n_of_frames adds up to 0.5
+        # we need it to add to to 1 so that in total the manipulator rotates by the specified angles
+        thetas_sliced.append(thetas[j] * (2 * frame / n_of_frames))
+    r.forwardKinm(thetas_sliced)
+    toBase = [np.array([[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,1]], dtype=np.float32)]
+    x_hat_dat =[[0],[0],[0]]
+    y_hat_dat =[[0],[0],[0]]
+    z_hat_dat =[[0],[0],[0]]
+    p_dat = [[0],[0],[0]]
+    for j in range(len(r.joints)):
+        toBase.append(toBase[-1] @ r.joints[j].HomMat)
+        orientation = toBase[-1][0:3,0:3] 
+        x_hat = orientation[0:3,0]  + toBase[-1][0:3,3]
+        y_hat = orientation[0:3,1] + toBase[-1][0:3,3]
+        z_hat = orientation[0:3,2] + toBase[-1][0:3,3]
+        p = (-1* ( toBase[-2][0:3,3] - toBase[-1][0:3,3] ) + toBase[-2][0:3,3])
+        for i in range(3):
+            x_hat_dat[i].append(x_hat[i])
+            y_hat_dat[i].append(y_hat[i])
+            z_hat_dat[i].append(z_hat[i])
+            p_dat[i].append(p[i])
+#            r.lines[j][0].set_data(x_hat_dat[0], x_hat_dat[1])
+#            r.lines[j][0].set_3d_properties(x_hat_dat[2])
+#            r.lines[j][1].set_data(y_hat_dat[0], y_hat_dat[1])
+#            r.lines[j][1].set_3d_properties(y_hat_dat[2])
+#            r.lines[j][2].set_data(z_hat_dat[0], z_hat_dat[1])
+#            r.lines[j][2].set_3d_properties(z_hat_dat[2])
+            r.lines[j][3].set_data(p_dat[0], p_dat[1])
+            r.lines[j][3].set_3d_properties(p_dat[2])
+#    print(frame / frame_length)
+    if frame == 1:
+        r.drawState(ax, 'r')
+
diff --git a/python/ur_simple_control/visualize/robot_stuff/inv_kinm.py b/python/ur_simple_control/visualize/robot_stuff/inv_kinm.py
new file mode 100644
index 0000000000000000000000000000000000000000..2906bfd60219df5c7150a1d3d26425a7b1f71318
--- /dev/null
+++ b/python/ur_simple_control/visualize/robot_stuff/inv_kinm.py
@@ -0,0 +1,434 @@
+from robot_stuff.forw_kinm import *
+#from anim_func import *
+import numpy as np
+import matplotlib.pyplot as plt 
+import mpl_toolkits.mplot3d.axes3d as p3
+from matplotlib.animation import FuncAnimation
+import matplotlib.colors as colr
+import sys
+import scipy.optimize
+from qpsolvers import solve_qp
+from qpsolvers import dense_solvers 
+from scipy.linalg import sqrtm
+
+
+
+# hardcoded for all joints
+# of course each joint can have its own limit
+def clampVelocity(del_thet):
+    for indeks in range(len(del_thet)):
+        if del_thet[indeks] > 3.0:
+            del_thet[indeks] = 3.0 
+        
+        if del_thet[indeks] < -3.0:
+            del_thet[indeks] = -3.0 
+    return del_thet
+# r is the Robot_raw
+# t is the target position
+
+
+
+def invKinm_Jac_T(r, t):
+    e = t - r.p_e
+    num = np.dot(e, r.jac_tri @ r.jac_tri.T @ e)
+    den = np.dot(r.jac_tri @ r.jac_tri.T @ e, r.jac_tri @ r.jac_tri.T @ e)
+    alpha = num / den
+    del_thet = alpha * r.jac_tri.T @ e
+
+# clamping for joint rotation limits
+    del_thet = clampVelocity(del_thet)
+
+# if you want a damping constant other than alpha
+#        del_thet = 0.011 * r.jac_tri.T @ e
+
+    return del_thet
+
+
+# using the nullspace for the comfort function
+# when doing manipulability, use the nullspace then go to vec_toward_greater_manip
+def invKinm_PseudoInv(r, t):
+    e = t - r.p_e
+
+    psedo_inv = np.linalg.pinv(r.jac_tri)
+    del_thet = psedo_inv @ e
+# we can add any nulspace vector to del_thet
+# and given the constraints, we should implement some sort of a comfort function
+# the min and max theta for each angle are hardcoded, but that will be changed 
+# they are hardcoded to +/- pi # 3/4 with center at 0
+# thus for all i q_iM - q_im = pi * 6/4
+# the added q_0 must be left multiplyed by (np.eye(n) - np.linalg.pinv(r.jac_tri) @ r.jac_tri)
+# we take into account the current theta (punish more if closer to the limit)
+# the formula is 3.57 in siciliano 
+    theta_for_limits = []
+    for k in range(r.ndof):
+        theta_for_limits.append( (-1/r.ndof) * (r.joints[k].theta / (np.pi * 1.5)))
+    theta_for_limits = np.array(theta_for_limits, dtype=np.float32)
+
+    del_thet += (np.eye(r.ndof) - psedo_inv @ r.jac_tri) @ theta_for_limits
+
+    del_thet = clampVelocity(del_thet)
+
+    return del_thet
+
+# what is this, i don't know
+def invKinm_PseudoInv_half(r, t):
+    e = t - r.p_e
+
+    #psedo_inv = np.linalg.pinv(r.jac_tri)
+    #psedo_inv = r.jac_tri.T @ sqrtm(r.jac_tri @ r.jac_tri.T)
+    psedo_inv = r.jac_tri.T @ np.linalg.inv(sqrtm(r.jac_tri @ r.jac_tri.T))
+    #psedo_inv =  sqrtm(r.jac_tri.T @ r.jac_tri) @ r.jac_tri.T
+    del_thet = psedo_inv @ e
+#    theta_for_limits = []
+#    for k in range(r.ndof):
+#        theta_for_limits.append( (-1/r.ndof) * (r.joints[k].theta / (np.pi * 1.5)))
+#    theta_for_limits = np.array(theta_for_limits, dtype=np.float32)
+#
+#    del_thet += (np.eye(r.ndof) - psedo_inv @ r.jac_tri) @ theta_for_limits
+
+    del_thet = clampVelocity(del_thet)
+
+    return del_thet
+
+
+
+
+# using the nullspace singularity avoidance
+def invKinmSingAvoidance_PseudoInv(r, e):
+#    e = t - r.p_e
+
+    psedo_inv = np.linalg.pinv(r.jac_tri)
+    del_thet = psedo_inv @ e
+# we can add any nulspace vector to del_thet
+# and given the constraints, we should implement some sort of a comfort function
+# the min and max theta for each angle are hardcoded, but that will be changed 
+# they are hardcoded to +/- pi # 3/4 with center at 0
+# thus for all i q_iM - q_im = pi * 6/4
+# the added q_0 must be left multiplyed by (np.eye(n) - np.linalg.pinv(r.jac_tri) @ r.jac_tri)
+# we take into account the current theta (punish more if closer to the limit)
+# the formula is 3.57 in siciliano 
+    gradMtoE = r.calcMToEGradient_kM()
+#    print(gradMtoE)
+
+    del_thet += (np.eye(r.ndof) - psedo_inv @ r.jac_tri) @ gradMtoE
+
+    del_thet = clampVelocity(del_thet)
+
+    return del_thet
+
+
+
+
+def invKinm_dampedSquares(r, t):
+    e = t - r.p_e
+#    e = np.array([0.0,0.0,-1.0], dtype=np.float32)
+#    e = np.array([0.0,1.0,0.0], dtype=np.float32)
+#    e = np.array([1.0,0.0,0.0], dtype=np.float32)
+    lamda = 0.3
+    iden = np.array([[1.,0.,0.], [0.,1.,0.], [0.,0.,1.]], dtype=np.float32)
+
+    del_thet = r.jac_tri.T @ np.linalg.inv(r.jac_tri @ r.jac_tri.T + lamda**2 * iden) @ e
+
+    del_thet = clampVelocity(del_thet)
+
+    # let's try to use the calculation which uses svd
+    # the derivation is in the iksurvey section 6
+    # the final equation is (11) and it calculates all left of e in above del_thet
+
+    # something is wrong here and i have  no idea what
+#    m = 3
+#    n = len(r.jac_tri[0,:])
+#    svdd = np.zeros((n, m))
+#    svd = np.linalg.svd(r.jac_tri) # the output is a list of 3 matrices: U, D and V
+#    # important note: D is not returned as a diagonal matrix, but as the list of diagonal entries
+#    for s in range(m): # 3 is the maximum rank of jacobian
+#        svdd = svdd + (svd[1][s] / (svd[1][s] ** 2 + lamda ** 2)) * svd[2][:,s].reshape(n,1) @ svd[0][:,s].reshape(1, m)
+
+
+#        del_thet = svdd @ e
+#        del_thet = clampVelocity(del_thet)
+
+    return del_thet
+
+
+def invKinmQP(r, t):
+    P = np.eye(r.ndof, dtype="double")
+    q = np.array([0] * r.ndof, dtype="double") # should be q imo
+    #G = np.eye(r.ndof, dtype="double")
+    G = None
+    e = t - r.p_e
+#        e = np.array([0.0, 1.0, 0.0])
+    b = np.array(e, dtype="double")
+    A = np.array(r.jac_tri, dtype="double")
+    #lb = np.array([-3] * r.ndof, dtype="double")
+    lb = None
+    #ub = np.array([3] * r.ndof, dtype="double")
+    ub = None
+    #h = ub
+    h = None
+ 
+ 
+    del_thet = solve_qp(P, q, G, h, A, b, lb, ub, solver="ecos")
+    #del_thet = solve_qp(P, q, G, h, A, b, lb, ub, solver="quadprog")
+ 
+    return del_thet
+
+
+
+
+
+# qp formulation, solved with scipy
+def invKinmGradDesc(r, t):
+    
+    def getEEPos(thetas, r, t):
+        p_e = r.eePositionAfterForwKinm(thetas)
+        e = t - p_e
+        error = np.sqrt(np.dot(e,e))
+        return error
+    
+    def toOptim(thetas):
+        #return np.sqrt(np.dot(thetas, thetas))
+        return np.dot(thetas, thetas)
+    e = t - r.p_e
+    lb = []
+    ub = []
+
+    def constraint(r, e):
+        # jac_tri @ del_thet must be equal to e
+        # when doing manipulability optimization it will be e + vec_toward_greater_manip
+        return scipy.optimize.LinearConstraint(r.jac_tri, e, e)
+
+
+    for bo in range(len(r.joints)):
+        lb.append(-3.0)
+        ub.append(3.0)
+    bounds = scipy.optimize.Bounds(lb, ub)
+
+    error = np.sqrt(np.dot(e,e))
+    thetas_start = []
+    for th in range(r.ndof):
+        thetas_start.append(r.joints[th].theta)
+    thetas_start = np.array(thetas_start, dtype=np.float32)
+
+    lin_constraint = constraint(r, e)
+    if (r.clamp == 1):
+        res = scipy.optimize.minimize(toOptim, thetas_start, method='SLSQP', constraints=lin_constraint, bounds=bounds)
+    else:
+        res = scipy.optimize.minimize(toOptim, thetas_start, method='SLSQP', constraints=lin_constraint)
+#        res = scipy.optimize.minimize(getEEPos, thetas_start, args=(r,t), method='SLSQP', constraints=lin_constraint, bounds=bounds)
+#        res = scipy.optimize.minimize(toOptim, thetas_start, method='CG', bounds=bounds)
+    # without constraints it returns some crazy big numbres like 10**300 or sth
+    # so something is seriously wrong there
+    del_thet = []
+    for bla in range(len(res.x)):
+        del_thet.append(float(res.x[bla]))
+#            del_thet.append(res.x[bla] - 0.01)
+#        for bla in range(len(res.x)):
+#            del_thet.append(float(res.x[bla]))
+#            del_thet[bla] += 0.01
+#            print(del_thet[bla])
+#        print("del_thet")
+#        print(del_thet)
+
+#        del_thet = np.array(del_thet, dtype=np.float32)
+    del_thet = clampVelocity(del_thet)
+    return del_thet
+
+
+
+###############################################################
+# IK with singularity avoidance via QP
+###############################################################
+
+# qp formulation, solved with scipy
+def invKinmSingAvoidanceWithQP_kM(r, t):
+    
+    def getEEPos(thetas, r, t):
+        p_e = r.eePositionAfterForwKinm(thetas)
+        e = t - p_e
+        error = np.sqrt(np.dot(e,e))
+        return error
+    
+    # E is shere toward which the manipulability elipsoid M should move
+    # the calculation is defined as a method in the robot_raw class
+    def toOptim(thetas, r):
+        # alpha is a coef to select the amount of moving toward E
+#        aplha = 3.03
+        grad_to_E = r.calcMToEGradient_kM()
+#        return np.dot(thetas, thetas) #+ coef_to_E @ thetas
+        return np.dot(thetas, thetas) + np.dot(grad_to_E, thetas)
+    e = t - r.p_e
+    lb = []
+    ub = []
+    def constraint(r, e):
+        # jac_tri @ del_thet must be equal to e
+        # when doing manipulability optimization it will be e + vec_toward_greater_manip
+        return scipy.optimize.LinearConstraint(r.jac_tri, e, e)
+
+
+    for bo in range(r.ndof):
+        lb.append(-3.0)
+        ub.append(3.0)
+    bounds = scipy.optimize.Bounds(lb, ub)
+
+    error = np.sqrt(np.dot(e,e))
+    thetas_start = []
+    for th in range(r.ndof):
+        thetas_start.append(r.joints[th].theta)
+    thetas_start = np.array(thetas_start, dtype=np.float32)
+
+    lin_constraint = constraint(r, e)
+    if (r.clamp == 1):
+        res = scipy.optimize.minimize(toOptim, thetas_start, args=(r), method='SLSQP', constraints=lin_constraint, bounds=bounds)
+    else:
+        res = scipy.optimize.minimize(toOptim, thetas_start, args=(r), method='SLSQP', constraints=lin_constraint)
+#        res = scipy.optimize.minimize(toOptim, thetas_start, args=(r), method='CG', constraints=lin_constraint)
+#        res = scipy.optimize.minimize(getEEPos, thetas_start, args=(r,t), method='SLSQP', constraints=lin_constraint, bounds=bounds)
+#        res = scipy.optimize.minimize(toOptim, thetas_start, method='CG', bounds=bounds)
+    # without constraints it returns some crazy big numbres like 10**300 or sth
+    # so something is seriously wrong there
+    del_thet = []
+    for bla in range(len(res.x)):
+        del_thet.append(float(res.x[bla]))
+#            del_thet.append(res.x[bla] - 0.01)
+#        for bla in range(len(res.x)):
+#            del_thet.append(float(res.x[bla]))
+#            del_thet[bla] += 0.01
+#            print(del_thet[bla])
+#        print("del_thet")
+#        print(del_thet)
+
+#        del_thet = np.array(del_thet, dtype=np.float32)
+#    print(del_thet)
+    del_thet = clampVelocity(del_thet)
+    return del_thet
+
+
+
+def invKinmSingAvoidanceWithQP_kI(r, t):
+    
+    def getEEPos(thetas, r, t):
+        p_e = r.eePositionAfterForwKinm(thetas)
+        e = t - p_e
+        error = np.sqrt(np.dot(e,e))
+        return error
+    
+    # E is shere toward which the manipulability elipsoid M should move
+    # the calculation is defined as a method in the robot_raw class
+    def toOptim(thetas, r):
+        # alpha is a coef to select the amount of moving toward E
+#        aplha = 3.03
+        grad_to_E = r.calcMToEGradient_kI()
+#        return np.dot(thetas, thetas) #+ coef_to_E @ thetas
+        return np.dot(thetas, thetas) + np.dot(grad_to_E, thetas)
+    e = t - r.p_e
+    lb = []
+    ub = []
+    def constraint(r, e):
+        # jac_tri @ del_thet must be equal to e
+        # when doing manipulability optimization it will be e + vec_toward_greater_manip
+        return scipy.optimize.LinearConstraint(r.jac_tri, e, e)
+
+
+    for bo in range(r.ndof):
+        lb.append(-3.0)
+        ub.append(3.0)
+    bounds = scipy.optimize.Bounds(lb, ub)
+
+    error = np.sqrt(np.dot(e,e))
+    thetas_start = []
+    for th in range(r.ndof):
+        thetas_start.append(r.joints[th].theta)
+    thetas_start = np.array(thetas_start, dtype=np.float32)
+
+    lin_constraint = constraint(r, e)
+    if (r.clamp == 1):
+        res = scipy.optimize.minimize(toOptim, thetas_start, args=(r), method='SLSQP', constraints=lin_constraint, bounds=bounds)
+    else:
+        res = scipy.optimize.minimize(toOptim, thetas_start, args=(r), method='SLSQP', constraints=lin_constraint)
+#        res = scipy.optimize.minimize(toOptim, thetas_start, args=(r), method='CG', constraints=lin_constraint)
+#        res = scipy.optimize.minimize(getEEPos, thetas_start, args=(r,t), method='SLSQP', constraints=lin_constraint, bounds=bounds)
+#        res = scipy.optimize.minimize(toOptim, thetas_start, method='CG', bounds=bounds)
+    # without constraints it returns some crazy big numbres like 10**300 or sth
+    # so something is seriously wrong there
+    del_thet = []
+    for bla in range(len(res.x)):
+        del_thet.append(float(res.x[bla]))
+#            del_thet.append(res.x[bla] - 0.01)
+#        for bla in range(len(res.x)):
+#            del_thet.append(float(res.x[bla]))
+#            del_thet[bla] += 0.01
+#            print(del_thet[bla])
+#        print("del_thet")
+#        print(del_thet)
+
+#        del_thet = np.array(del_thet, dtype=np.float32)
+#    print(del_thet)
+    del_thet = clampVelocity(del_thet)
+    return del_thet
+
+
+
+def invKinmQPSingAvoidE_kI(r, t):
+    P = np.eye(r.ndof, dtype="double")
+#    q = 0.5 * np.array(r.calcMToEGradient_kI(), dtype="double")
+    q = np.array(r.calcMToEGradient_kI(), dtype="double")
+#    G = np.eye(r.ndof, dtype="double")
+#    G = []
+    G = None
+    e = t - r.p_e
+    b = np.array(e, dtype="double")
+    A = np.array(r.jac_tri, dtype="double")
+    #lb = np.array([-3] * r.ndof, dtype="double")
+    lb = None
+    #ub = np.array([3] * r.ndof, dtype="double")
+    ub = None
+#    h = ub 
+    h = None
+ 
+#    del_thet = solve_qp(P, q, G, h, A, b, lb, ub, solver="quadprog")
+    del_thet = solve_qp(P, q, G, h, A, b, lb, ub, solver="ecos")
+ 
+    return del_thet
+
+
+
+def invKinmQPSingAvoidE_kM(r, t):
+    P = np.eye(r.ndof, dtype="double")
+#    q = 0.5 * np.array(r.calcMToEGradient_kM(), dtype="double")
+    q = np.array(r.calcMToEGradient_kM(), dtype="double")
+    #G = np.eye(r.ndof, dtype="double")
+    G = None
+    e = t - r.p_e
+#        e = np.array([0.0, 1.0, 0.0])
+    b = np.array(e, dtype="double")
+    A = np.array(r.jac_tri, dtype="double")
+    #lb = np.array([-3] * r.ndof, dtype="double")
+    #ub = np.array([3] * r.ndof, dtype="double")
+    lb = None
+    ub = None
+    #h = ub
+    h = None
+ 
+ 
+    del_thet = solve_qp(P, q, G, h, A, b, lb, ub, solver="ecos")
+ 
+    return del_thet
+
+
+
+def invKinmQPSingAvoidManipMax(r, t):
+    P = np.eye(r.ndof, dtype="double")
+    q = np.array(r.calcManipMaxGrad(), dtype="double")
+    G = None
+    e = t - r.p_e
+    b = np.array(e, dtype="double")
+    A = np.array(r.jac_tri, dtype="double")
+    lb = np.array([-3] * r.ndof, dtype="double")
+    ub = np.array([3] * r.ndof, dtype="double")
+    h = None
+ 
+    del_thet = solve_qp(P, q, G, h, A, b, lb, ub, solver="ecos")
+ 
+    return del_thet
+
diff --git a/python/ur_simple_control/visualize/robot_stuff/joint_as_hom_mat.py b/python/ur_simple_control/visualize/robot_stuff/joint_as_hom_mat.py
new file mode 100644
index 0000000000000000000000000000000000000000..66233e10d9ab1a511580539ffb00b6af3a9f18ce
--- /dev/null
+++ b/python/ur_simple_control/visualize/robot_stuff/joint_as_hom_mat.py
@@ -0,0 +1,146 @@
+import numpy as np
+
+"""
+Here we formalize the robot as a series of rigid bodies (links)
+connected with joints. Each joint is endowed with a coordinate frame
+which is derived via the Denavit-Hartenberg parameters. 
+Here all joints are revolute, meaning rotating, and rotations are 
+reprezented as rotation matrices.
+Rotating matrices and translation vectors are combined
+into homogenous matrices and then a frame change amounts
+to matrix multiplication.
+"""
+
+# deriving the homogeneus transformation matrix
+# as a series of transformations between two coordinate frames
+# detailed description: Wikipedia article on Denavit-Hartenberg parameters
+
+# this function is wrong!!!!!!!!!!!!!1
+def DHtoHomMat(d, theta, r, alpha):
+    ct = np.cos(theta)
+    st = np.sin(theta)
+    ca = np.cos(alpha)
+    sa = np.sin(alpha)
+
+    # the d: translation along i^th z-axis until the common normal
+    trans_z_n_prev = np.array([[1,0,0], [0,1,0], [0,0,1]], dtype=np.float32)
+    trans_z_n_prev = np.hstack((trans_z_n_prev, np.array([0,0,d], dtype=np.float32).reshape(3,1)))
+    trans_z_n_prev = np.vstack((trans_z_n_prev, np.array([0,0,0,1], dtype=np.float32)))
+
+    # the theta: rotation about i^th z-axis until i^th x-axis coincides with the common normal
+    # this is the parameter we get to control
+    rot_z_n_prev = np.array([[ct,-1*st,0], [st,ct,0], [0,0,1]], dtype=np.float32)
+    rot_z_n_prev= np.hstack((rot_z_n_prev, np.array([0,0,1], dtype=np.float32).reshape(3,1)))
+    rot_z_n_prev= np.vstack((rot_z_n_prev, np.array([0,0,0,1], dtype=np.float32)))
+    
+    # the r: traslation along the i^th x-axis, now common normal, until the origin of i+1^th frame
+    trans_x_n = np.array([[1,0,0], [0,1,0], [0,0,1]], dtype=np.float32)
+    trans_x_n = np.hstack((trans_x_n, np.array([r,0,0], dtype=np.float32).reshape(3,1)))
+    trans_x_n = np.vstack((trans_x_n, np.array([0,0,0,1], dtype=np.float32)))
+    
+    # the a: rotation about common normal so that z-axis aling 
+    rot_x_n = np.array([[1,0,0], [0,ca,-1*sa], [0,sa,ca]], dtype=np.float32)
+    rot_x_n = np.hstack((rot_x_n, np.array([0,0,0], dtype=np.float32).reshape(3,1)))
+    rot_x_n = np.vstack((rot_x_n, np.array([0,0,0,1], dtype=np.float32)))
+
+    return trans_z_n_prev @ rot_z_n_prev @ trans_x_n @ rot_x_n
+
+# the above, but multiplied into the final form (less calculation needed, hence better)
+# also correct
+def createDHMat(d, theta, r, alpha):
+    ct = np.cos(theta)
+    st = np.sin(theta)
+    ca = np.cos(alpha)
+    sa = np.sin(alpha)
+    DHMat = np.array([ [ct, -1 * st * ca, st * sa,      r * ct], \
+                       [st,      ct * ca, -1 * ct * sa, r * st], \
+                       [0,        sa,        ca,         d], \
+                       [0,        0,       0,           1]  ], dtype=np.float32)
+    return DHMat
+
+
+def createModifiedDHMat(d, theta, r, alpha):
+    ct = np.cos(theta)
+    st = np.sin(theta)
+    ca = np.cos(alpha)
+    sa = np.sin(alpha)
+    MDHMat = np.array( [[ct, -1* st, 0, r], \
+                        [st * ca, ct * ca, -1* sa, -d * sa], \
+                        [st * sa, ct * sa, ca, d * ca], \
+                        [0, 0, 0, 1]], dtype=np.float32)
+    return MDHMat
+
+
+# clamping of all joints to rotate only by 7/8 of the cirle
+# of course this can be changed to other limits as needed
+# however, it is hardcoded here because it's good enough for demonstrative purposes
+# the ur arms all do not have joint limits!
+def clampTheta(theta):
+    if theta > np.pi * 7/4:
+        theta = np.pi * 7/4
+    if theta < -1 * (np.pi * 7/4):
+        theta = -1 * (np.pi * 7/4)
+    return theta
+
+# if clamp is == 0 then don't clamp and if it is 1 then do clamp the angles
+# this class does not reprezent a geometric entity, but rather the coordinate system
+# in which kinematics is defined for a particular joint
+# and that coordinate system is defined via the D-H parameters
+class Joint:
+    def __init__(self, d, theta, r, alpha, clamp):
+        self.clamp = clamp
+# potential clamping for joint rotation limits
+        if clamp == 1:
+            self.theta = clampTheta(theta)
+        else:
+            self.theta = theta
+        self.d = d
+        self.r = r
+        self.alpha = alpha
+        self.ct = np.cos(theta)
+        self.st = np.sin(theta)
+        self.ca = np.cos(alpha)
+        self.sa = np.sin(alpha)
+        self.HomMat = createDHMat(self.d, self.theta, self.r, self.alpha)
+   
+
+# we simply recalculate the homogenous transformation matrix for the given joint
+# and that equates to it being rotated
+# the timely rotation is simply the rotation broken into discete parts
+########################################################################
+# this is fundamentaly different from how the physics simulation works!
+# there rotation equates to setting positions/velocities to individual motors,
+# which then do the moving.
+# there you can only SENSE via SENSORS what the result of the rotation is
+# in this brach we essentially just do numerics!
+    def updateDHMat(self):
+        self.ct = np.cos(self.theta)
+        self.st = np.sin(self.theta)
+        self.HomMat = np.array([ [self.ct, -1 * self.st * self.ca, self.st * self.sa,      self.r * self.ct], \
+                           [self.st,      self.ct * self.ca, -1 * self.ct * self.sa, self.r * self.st], \
+                           [0,        self.sa,        self.ca,         self.d], \
+                           [0,        0,       0,           1]  ], dtype=np.float32)
+
+
+    def rotate_numerically(self,theta, clamp):
+# potential clamping for joint rotation limits
+        if self.clamp == 1:
+            self.theta = clampTheta(theta)
+        else:
+            self.theta = theta % (2 * np.pi)
+        #    self.theta = theta 
+        #self.HomMat = createDHMat(self.d, self.theta, self.r, self.alpha)
+        self.updateDHMat()
+
+    def rotate_with_MDH(self, theta, clamp):
+# potential clamping for joint rotation limits
+        if self.clamp == 1:
+            self.theta = clampTheta(theta)
+        else:
+            self.theta = theta % (2 * np.pi)
+            #self.theta = theta
+        self.HomMat = createModifiedDHMat(self.d, self.theta, self.r, self.alpha)
+
+
+    def __repr__(self):
+        return str(self.HomMat)
diff --git a/python/ur_simple_control/visualize/robot_stuff/utils.py b/python/ur_simple_control/visualize/robot_stuff/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..e3596956983c4070fef67f18f0eb9f776d236203
--- /dev/null
+++ b/python/ur_simple_control/visualize/robot_stuff/utils.py
@@ -0,0 +1,36 @@
+import numpy as np
+
+
+def error_test(p_e, target):
+    e = np.abs(target - p_e)
+    if e[0] < 0.002 and e[1] < 0.002 and e[2] < 0.002:
+        return True
+    else:
+        return False
+
+def goal_distance(achieved_goal, goal):
+    return np.linalg.norm(goal - achieved_goal)
+
+
+def calculateManipulabilityIndex(robot):
+    M = robot.jac_tri @ robot.jac_tri.T
+    return np.sqrt(np.linalg.det(M))
+
+def calculateSmallestManipEigenval(robot):
+    M = robot.jac_tri @ robot.jac_tri.T
+    diagonal_of_svd_of_M = np.linalg.svd(M)[1]
+    return diagonal_of_svd_of_M[diagonal_of_svd_of_M.argmin()]
+
+def calculatePerformanceMetrics(robot):
+    M = robot.jac_tri @ robot.jac_tri.T
+    diagonal_of_svd_of_M = np.linalg.svd(M)[1]
+    singularity = 0 
+    try:
+        M_inv = np.linalg.inv(M)
+    except np.linalg.LinAlgError as e:
+        print("ROKNUH U SINGULARITET!!!!!!!!!!!")
+        singularity = 1
+    return {'manip_index': np.sqrt(np.linalg.det(M)),
+            'smallest_eigenval': diagonal_of_svd_of_M[diagonal_of_svd_of_M.argmin()],
+            'singularity':singularity }
+
diff --git a/python/ur_simple_control/visualize/robot_stuff/webots_api_helper_funs.py b/python/ur_simple_control/visualize/robot_stuff/webots_api_helper_funs.py
new file mode 100644
index 0000000000000000000000000000000000000000..8299d4bc6bb4c6439992d00da518c05bda66b0c0
--- /dev/null
+++ b/python/ur_simple_control/visualize/robot_stuff/webots_api_helper_funs.py
@@ -0,0 +1,191 @@
+#########################################################3
+# for now only for ur10e, later write out for other robots
+#########################################################3
+import numpy as np
+
+
+def getAllMotors(robot):
+    motors = []
+    motors.append(robot.getMotor('shoulder_pan_joint'))
+    motors.append(robot.getMotor('shoulder_lift_joint'))
+    motors.append(robot.getMotor('elbow_joint'))
+    motors.append(robot.getMotor('wrist_1_joint'))
+    motors.append(robot.getMotor('wrist_2_joint'))
+    motors.append(robot.getMotor('wrist_3_joint'))
+    print("imported motors")
+    return motors
+
+
+# for validation purposes, we must plot this circle
+def drawCircle(radius, height, robot):
+    display = robot.getDisplay("display")
+#    display.setColor(0xFFFFF)
+    display.setColor(0xF8FF04)
+    display.setColor(0xF8FF04)
+    display.setColor(0xFF0022)
+    display.setColor(0xFF0022)
+    display.setColor(0x00FFE6)
+    display.setColor(0x00FFE6)
+    display.drawOval(100,100,60,60)
+    display.drawOval(100,100,60,60)
+    display.drawOval(100,100,59,59)
+    display.drawOval(100,100,59,59)
+    print("drew the circle on the screen")
+
+def setMotorSpeeds(motors, speeds):
+    for i in range(len(motors)):
+        motors[i].setVelocity(speeds[i])
+
+
+
+def initializeMotorsForVelocity(motors):
+    speeds = []
+    for motor in motors:
+        motor.setPosition(float('inf'))
+        speeds.append(0)
+
+    
+    setMotorSpeeds(motors, speeds)
+
+
+
+def initializeMotorsForPosition(motors):
+    speeds = []
+    for motor in motors:
+#        motor.setVelocity(float('inf'))
+        speeds.append(0)
+
+    
+    setMotorSpeeds(motors, speeds)
+    print("did motor init")
+
+
+def getAndInitAllSensors(robot):
+    sensors = []
+    sensors.append(robot.getPositionSensor('shoulder_pan_joint_sensor'))
+    sensors.append(robot.getPositionSensor('shoulder_lift_joint_sensor'))
+    sensors.append(robot.getPositionSensor('elbow_joint_sensor'))
+    sensors.append(robot.getPositionSensor('wrist_1_joint_sensor'))
+    sensors.append(robot.getPositionSensor('wrist_2_joint_sensor'))
+    sensors.append(robot.getPositionSensor('wrist_3_joint_sensor'))
+   
+   # now we must specify how often do sensors get read in miliseconds
+   # i've put 10ms since this seems like it should work smoothly,
+   # but ofc you should play with this value later on
+    for sensor in sensors:
+        sensor.enable(10)
+
+    print("imported and inited sensors")
+    return sensors
+
+def readJointState(sensors):
+    joint_positions = []
+    for sensor in sensors:
+        joint_positions.append(sensor.getValue())
+    return joint_positions
+
+
+
+
+
+def getAllMotorsJaco(robot):
+    motors = []
+    motors.append(robot.getMotor('j2n6s300_joint_1'))
+    motors.append(robot.getMotor('j2n6s300_joint_2'))
+    motors.append(robot.getMotor('j2n6s300_joint_3'))
+    motors.append(robot.getMotor('j2n6s300_joint_4'))
+    motors.append(robot.getMotor('j2n6s300_joint_5'))
+    motors.append(robot.getMotor('j2n6s300_joint_6'))
+#    motors.append(robot.getMotor('j2n6s300_joint7'))
+    print("imported motors")
+    return motors
+
+
+def getAndInitAllSensorsJaco(robot):
+    sensors = []
+    sensors.append(robot.getPositionSensor('j2n6s300_joint_1_sensor'))
+    sensors.append(robot.getPositionSensor('j2n6s300_joint_2_sensor'))
+    sensors.append(robot.getPositionSensor('j2n6s300_joint_3_sensor'))
+    sensors.append(robot.getPositionSensor('j2n6s300_joint_4_sensor'))
+    sensors.append(robot.getPositionSensor('j2n6s300_joint_5_sensor'))
+    sensors.append(robot.getPositionSensor('j2n6s300_joint_6_sensor'))
+#    sensors.append(robot.getPositionSensor('j2n6s300_joint7_sensor'))
+   
+   # now we must specify how often do sensors get read in miliseconds
+   # i've put 10ms since this seems like it should work smoothly,
+   # but ofc you should play with this value later on
+    for sensor in sensors:
+        sensor.enable(10)
+
+    print("imported and inited sensors")
+    return sensors
+
+
+
+
+def getAllMotorsKuka(robot):
+    motors = []
+    motors.append(robot.getMotor('joint_a1'))
+    motors.append(robot.getMotor('joint_a2'))
+    motors.append(robot.getMotor('joint_a3'))
+    motors.append(robot.getMotor('joint_a4'))
+    motors.append(robot.getMotor('joint_a5'))
+    motors.append(robot.getMotor('joint_a6'))
+    motors.append(robot.getMotor('joint_a7'))
+    print("imported motors")
+    return motors
+
+
+def getAndInitAllSensorsKuka(robot):
+    sensors = []
+    sensors.append(robot.getPositionSensor('joint_a1_sensor'))
+    sensors.append(robot.getPositionSensor('joint_a2_sensor'))
+    sensors.append(robot.getPositionSensor('joint_a3_sensor'))
+    sensors.append(robot.getPositionSensor('joint_a4_sensor'))
+    sensors.append(robot.getPositionSensor('joint_a5_sensor'))
+    sensors.append(robot.getPositionSensor('joint_a6_sensor'))
+    sensors.append(robot.getPositionSensor('joint_a7_sensor'))
+   
+   # now we must specify how often do sensors get read in miliseconds
+   # i've put 10ms since this seems like it should work smoothly,
+   # but ofc you should play with this value later on
+    for sensor in sensors:
+        sensor.enable(10)
+
+    print("imported and inited sensors")
+    return sensors
+
+
+
+def getAllMotorsJaco7(robot):
+    motors = []
+    motors.append(robot.getMotor('j2s7s300_joint_1'))
+    motors.append(robot.getMotor('j2s7s300_joint_2'))
+    motors.append(robot.getMotor('j2s7s300_joint_3'))
+    motors.append(robot.getMotor('j2s7s300_joint_4'))
+    motors.append(robot.getMotor('j2s7s300_joint_5'))
+    motors.append(robot.getMotor('j2s7s300_joint_6'))
+    motors.append(robot.getMotor('j2s7s300_joint_7'))
+    print("imported motors")
+    return motors
+
+
+def getAndInitAllSensorsJaco7(robot):
+    sensors = []
+    sensors.append(robot.getPositionSensor('j2s7s300_joint_1_sensor'))
+    sensors.append(robot.getPositionSensor('j2s7s300_joint_2_sensor'))
+    sensors.append(robot.getPositionSensor('j2s7s300_joint_3_sensor'))
+    sensors.append(robot.getPositionSensor('j2s7s300_joint_4_sensor'))
+    sensors.append(robot.getPositionSensor('j2s7s300_joint_5_sensor'))
+    sensors.append(robot.getPositionSensor('j2s7s300_joint_6_sensor'))
+    sensors.append(robot.getPositionSensor('j2s7s300_joint_7_sensor'))
+   
+   # now we must specify how often do sensors get read in miliseconds
+   # i've put 10ms since this seems like it should work smoothly,
+   # but ofc you should play with this value later on
+    for sensor in sensors:
+        sensor.enable(10)
+
+    print("imported and inited sensors")
+    return sensors
+