diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..bee8a64b79a99590d5303307144172cfe824fbf7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+__pycache__
diff --git a/curves.py b/curves.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a3aadc0351996ed48ead5c08ea7a90de3434c63
--- /dev/null
+++ b/curves.py
@@ -0,0 +1,120 @@
+import numpy as np
+import math
+
+
+class Curve:
+    def __call__(self, u):
+        raise NotImplementedError()
+
+    def derivative(self, u):
+        raise NotImplementedError()
+
+    def second_derivative(self, u):
+        raise NotImplementedError()
+
+
+class Line1D(Curve):
+    def __init__(self, a, b):
+        self.a = a
+        self.b = b
+
+    def __call__(self, u):
+        v = 1 - u
+        return (self.a * v + self.b * u)[:, np.newaxis]
+
+    def derivative(self, u):
+        return u[:, np.newaxis]*0.0 + (self.b - self.a)
+
+    def second_derivative(self, u):
+        return u[:, np.newaxis]*0.0
+
+
+class Line2D(Curve):
+    def __init__(self, a, b):
+        self.a = a
+        self.b = b
+
+    def __call__(self, u):
+        v = 1 - u
+        n = len(u)
+        p = np.zeros((n, 2))
+        p[:, 0] = (self.a[0] * v + self.b[0] * u)
+        p[:, 1] = (self.a[1] * v + self.b[1] * u)
+        return p
+
+    def derivative(self, u):
+        n = len(u)
+        p = np.zeros((n, 2))
+        p[:, 0] = (self.b[0] - self.a[0])
+        p[:, 1] = (self.b[1] - self.a[1])
+        return p
+
+    def second_derivative(self, u):
+        n = len(u)
+        p = np.zeros((n, 2))
+        return p
+
+
+class Circle2D(Curve):
+    def __init__(self, r):
+        self.r = r
+
+    def __call__(self, u):
+        n = len(u)
+        p = np.zeros((n, 2))
+        p[:, 0] = np.cos(2*math.pi*u)
+        p[:, 1] = np.sin(2*math.pi*u)
+        return p
+
+    def derivative(self, u):
+        n = len(u)
+        p = np.zeros((n, 2))
+        p[:, 0] = -2*math.pi*np.sin(2*math.pi*u)
+        p[:, 1] = 2*math.pi*np.cos(2*math.pi*u)
+        return p
+
+    def second_derivative(self, u):
+        n = len(u)
+        p = np.zeros((n, 2))
+        p[:, 0] = -4*math.pi*math.pi*np.cos(2*math.pi*u)
+        p[:, 1] = -4*math.pi*math.pi*np.sin(2*math.pi*u)
+        return p
+
+
+class CubicBezier(Curve):
+    def __init__(self, A, B, C, D):
+        self.n_dims = len(A)
+        self.A = A
+        self.B = B
+        self.C = C
+        self.D = D
+
+    def __call__(self, u):
+        v = 1-u
+        n = len(u)
+        p = np.zeros((n, self.n_dims))
+        for i in range(self.n_dims):
+            p[:, i] += (v*v*v)*self.A[i]
+            p[:, i] += 3*v*v*u*self.B[i]
+            p[:, i] += 3*v*u*u*self.C[i]
+            p[:, i] += u*u*u*self.D[i]
+        return p
+
+    def derivative(self, u):
+        v = 1-u
+        n = len(u)
+        p = np.zeros((n, self.n_dims))
+        for i in range(self.n_dims):
+            p[:, i] += 3*v*v*(self.B[i]-self.A[i])
+            p[:, i] += 6*v*u*(self.C[i]-self.B[i])
+            p[:, i] += 3*u*u*(self.D[i]-self.C[i])
+        return p
+
+    def second_derivative(self, u):
+        v = 1 - u
+        n = len(u)
+        p = np.zeros((n, self.n_dims))
+        for i in range(self.n_dims):
+            p[:, i] += 6*v*(self.C[i]-2*self.B[i]+self.A[i])
+            p[:, i] += 6*u*(self.D[i]-2*self.C[i]+self.B[i])
+        return p
diff --git a/curves/p.npy b/curves/p.npy
new file mode 100644
index 0000000000000000000000000000000000000000..8b34498dcf3181666b25fe14d6d3900ba5b1ad8b
Binary files /dev/null and b/curves/p.npy differ
diff --git a/curves/p_prime.npy b/curves/p_prime.npy
new file mode 100644
index 0000000000000000000000000000000000000000..a00dc31931dedb2d125dcff6fca6ca5b0728540b
Binary files /dev/null and b/curves/p_prime.npy differ
diff --git a/curves/p_prime2.npy b/curves/p_prime2.npy
new file mode 100644
index 0000000000000000000000000000000000000000..2ba851498aa33eaaa1b90fcb4cc53d2fcf424adc
Binary files /dev/null and b/curves/p_prime2.npy differ
diff --git a/curves/u.npy b/curves/u.npy
new file mode 100644
index 0000000000000000000000000000000000000000..db5f73d34a33111edf97ffafa41855a68631221a
Binary files /dev/null and b/curves/u.npy differ
diff --git a/curves/u_dot.npy b/curves/u_dot.npy
new file mode 100644
index 0000000000000000000000000000000000000000..8ddef6c76b92842f1593aa6ed25d31cbb35924e2
Binary files /dev/null and b/curves/u_dot.npy differ
diff --git a/plot_solution_video.py b/plot_solution_video.py
new file mode 100644
index 0000000000000000000000000000000000000000..cfa785dc8ea2ea20a66448d256af1a3684a7036d
--- /dev/null
+++ b/plot_solution_video.py
@@ -0,0 +1,117 @@
+import math
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.animation import FuncAnimation
+import time
+
+ani = None
+interval_ms = 16
+t = 0
+
+
+def update(frame, lines, u, u_dot, p, p_prime, p_prime2):
+    global ani, interval_ms, t
+
+    n = len(u)
+    u_dot_mid = (u_dot[:-1] + u_dot[1:])*0.5
+    t_delta = (u[1:] - u[:-1])/u_dot_mid
+    t_delta_ext = np.zeros((n,))
+    t_delta_ext[1:] = t_delta
+    t_axis = np.cumsum(t_delta_ext)
+    a = np.zeros((n-1, 2))
+    p_prime_mid = 0.5 * (p_prime[:-1, :] + p_prime[1:, :])
+    p_prime2_mid = 0.5 * (p_prime2[:-1, :] + p_prime2[1:, :])
+    u_dot_prime = (u_dot[1:] - u_dot[:-1]) / (u[1:] - u[:-1])
+    for i in range(2):
+        a[:, i] += p_prime2_mid[:, i] * u_dot_mid * u_dot_mid
+        a[:, i] += p_prime_mid[:, i] * u_dot_mid * u_dot_prime
+
+    if t > t_axis[-1]:
+        # cycle
+        t = 0
+        # time.sleep(0.5)
+
+    p_now = np.zeros((2,))
+    a_now = np.zeros((2,))
+    p_now[0] = np.interp(t, t_axis, p[:, 0])
+    p_now[1] = np.interp(t, t_axis, p[:, 1])
+    a_now[0] = np.interp(t, t_axis[:-1], a[:, 0])
+    a_now[1] = np.interp(t, t_axis[:-1], a[:, 1])
+
+    a_scale = 0.03
+
+    lines[0].set_xdata(p[:, 0])
+    lines[0].set_ydata(p[:, 1])
+    lines[1].set_xdata(p_now[0])
+    lines[1].set_ydata(p_now[1])
+    lines[2].set_xdata([p_now[0], p_now[0]+a_scale*a_now[0]])
+    lines[2].set_ydata([p_now[1], p_now[1]+a_scale*a_now[1]])
+
+    lines[4].set_xdata(t_axis)
+    lines[4].set_ydata(u_dot)
+    lines[5].set_xdata([t, t])
+    lines[5].set_ydata([0, np.max(u_dot)*1.5])
+
+    t += interval_ms/1000.0
+
+    return lines
+
+
+def main_func():
+    global ani, interval_ms
+
+    u = np.load("curves/u.npy")
+    u_dot = np.load("curves/u_dot.npy")
+    p = np.load("curves/p.npy")
+    p_prime = np.load("curves/p_prime.npy")
+    p_prime2 = np.load("curves/p_prime2.npy")
+    n = len(u)
+
+    u_mid = (u[:-1] + u[1:]) * 0.5
+    u_delta = u[1:] - u[:-1]
+    u_dot_mid = (u_dot[:-1] + u_dot[1:]) * 0.5
+
+    t_delta = u_delta / u_mid
+    a = (u_dot[1:] - u_dot[:-1]) / t_delta
+
+    t_delta_ext = np.zeros((n,))
+    t_delta_ext[1:] = t_delta
+    t = np.cumsum(t_delta_ext)
+
+    fig, axes = plt.subplots(1, 2)
+    ax = axes[0]
+    ax2 = axes[1]
+    ln0, = ax.plot([], [], 'b')
+    ln1, = ax.plot([], [], 'k.', ms=20)
+    ln2, = ax.plot([], [], 'r-')
+    ln3, = ax.plot([], [], 'g-')
+    ax.set_xlim([np.min(p[:, 0])-0.5, np.max(p[:, 0])+0.5])
+    ax.set_ylim([np.min(p[:, 1])-0.5, np.max(p[:, 1])+0.5])
+    ax.set_aspect("equal")
+    ax.set_xlabel("x")
+    ax.set_ylabel("y")
+    ax.set_title("Motion")
+    # ax.legend(["speed", "min", "max"])
+
+    ln4, = ax2.plot([], [], 'b')
+    ln5, = ax2.plot([], [], 'k')
+    ln6, = ax2.plot([], [], 'r')
+    ax2.set_xlim([0, 2])
+    ax2.set_ylim([0, 1])
+    ax2.set_xlabel("t")
+    ax2.set_ylabel("u_dot")
+    ax2.set_title("Curve reading speed")
+    # ax2.legend(["acc.", "min", "max"])
+
+    lines = [ln0, ln1, ln2, ln3, ln4, ln5, ln6]
+    plt.tight_layout()
+
+    def init():
+        return lines
+
+    ani = FuncAnimation(fig, update, init_func=init, fargs=(lines, u, u_dot, p, p_prime, p_prime2), blit=True, interval=interval_ms)
+    plt.show()
+
+
+if __name__ == "__main__":
+    main_func()
diff --git a/main_animate.py b/solve_animate.py
similarity index 100%
rename from main_animate.py
rename to solve_animate.py
diff --git a/solve_curves.py b/solve_curves.py
new file mode 100644
index 0000000000000000000000000000000000000000..021fcbd9a0aa9327659621a3bca610079f9043e2
--- /dev/null
+++ b/solve_curves.py
@@ -0,0 +1,211 @@
+import numpy as np
+import matplotlib.pyplot as plt
+import curves
+
+
+# def a_mp_func(v, v1=0.2, a0=7.0, ap=5.0, am=7.0):
+#     n = len(v)
+#     a_minus = np.zeros((n,))
+#     a_plus = np.zeros((n,))
+#     alpha1 = np.minimum(v[v > 0]/v1, 1)
+#     alpha2 = np.minimum(-v[v <= 0]/v1, 1)
+#     a_minus[v > 0] = -(alpha1 * am + (1-alpha1) * a0)
+#     a_plus[v > 0] = alpha1 * ap + (1-alpha1) * a0
+#     a_minus[v <= 0] = -(alpha2 * ap + (1-alpha2) * a0)
+#     a_plus[v <= 0] = alpha2 * am + (1-alpha2) * a0
+#     return a_minus, a_plus
+
+
+def a_mp_func(v, ap=5.0):
+    # basic, constant acceleration constraint
+    n = len(v)
+    a_minus = np.zeros((n,))
+    a_plus = np.zeros((n,))
+    a_plus[:] = ap
+    a_minus[:] = -ap
+    return a_minus, a_plus
+
+
+def constraints_a(p_prime, p_prime2, u, u_dot, jerk_max=50.0, show=False):
+    n, n_dims = p_prime.shape
+
+    a = np.zeros((n-1, n_dims))
+
+    a_minus = np.zeros((n-1, n_dims))
+    a_plus = np.zeros((n-1, n_dims))
+
+    u_mid = 0.5 * (u[:-1] + u[1:])
+    u_dot_mid = 0.5 * (u_dot[:-1] + u_dot[1:])
+    p_prime_mid = 0.5 * (p_prime[:-1, :] + p_prime[1:, :])
+    p_prime2_mid = 0.5 * (p_prime2[:-1, :] + p_prime2[1:, :])
+    u_dot_prime = (u_dot[1:] - u_dot[:-1]) / (u[1:] - u[:-1])
+    dt = (u[1:] - u[:-1]) / u_dot_mid
+    dt_a = (dt[:-1] + dt[1:]) * 0.5
+
+    u_dot2_min_all = -np.inf * np.ones((n - 1, n_dims))
+    u_dot2_max_all = np.inf * np.ones((n - 1, n_dims))
+
+    v = np.zeros((n-1, n_dims))
+    for i in range(n_dims):
+        v[:, i] = p_prime_mid[:, i] * u_dot_mid
+        a[:, i] += p_prime2_mid[:, i] * u_dot_mid * u_dot_mid
+        a[:, i] += p_prime_mid[:, i] * u_dot_mid * u_dot_prime
+
+        a_plus_next = a[:-1, i] + jerk_max * dt_a
+        a_minus_next = a[:-1, i] - jerk_max * dt_a
+        a_plus_prev = a[1:, i] + jerk_max * dt_a
+        a_minus_prev = a[1:, i] - jerk_max * dt_a
+
+        # boundary condition
+        # a_minus[0, i] = a_minus_prev[0]
+        # a_minus[-1, i] = a_minus_next[-1]
+        # a_plus[0, i] = a_plus_prev[0]
+        # a_plus[-1, i] = a_plus_next[-1]
+
+        # zero acceleration beyond curve
+        a_minus[0] = -jerk_max * dt_a[0]
+        a_plus[0] = jerk_max * dt_a[0]
+        a_minus[-1] = -jerk_max * dt_a[-1]
+        a_plus[-1] = jerk_max * dt_a[-1]
+
+        a_minus[1:-1, i] = np.maximum(a_minus_next[:-1], a_minus_prev[1:])
+        a_plus[1:-1, i] = np.minimum(a_plus_next[:-1], a_plus_prev[1:])
+
+        # min and max prescribed by speed
+        a_neg_bound, a_pos_bound = a_mp_func(v[:, i])
+        a_minus[:, i] = np.minimum(a_minus[:, i], a_pos_bound)
+        a_minus[:, i] = np.maximum(a_minus[:, i], a_neg_bound)
+        a_plus[:, i] = np.minimum(a_plus[:, i], a_pos_bound)
+        a_plus[:, i] = np.maximum(a_plus[:, i], a_neg_bound)
+
+        # acceleration along the curve only
+        a_plus_along = a_plus[:, i] - p_prime2_mid[:, i] * u_dot_mid*u_dot_mid
+        a_minus_along = a_minus[:, i] - p_prime2_mid[:, i] * u_dot_mid*u_dot_mid
+
+        mask = np.abs(p_prime_mid[:, i]) > 1e-7
+
+        u_dot2_1 = a_plus_along[mask]/p_prime_mid[mask, i]
+        u_dot2_2 = a_minus_along[mask]/p_prime_mid[mask, i]
+        u_dot2_min_all[mask, i] = np.minimum(u_dot2_1, u_dot2_2)
+        u_dot2_max_all[mask, i] = np.maximum(u_dot2_1, u_dot2_2)
+
+    u_dot2_min = np.max(u_dot2_min_all, axis=-1)
+    u_dot2_max = np.min(u_dot2_max_all, axis=-1)
+
+    if show:
+        plt.figure()
+        plt.plot(u_mid, u_dot_prime * u_dot_mid, 'k')
+        plt.plot(u_mid, u_dot2_min, 'b')
+        plt.plot(u_mid, u_dot2_max, 'r')
+        plt.title("u_dot2")
+
+        plt.figure()
+        for i in range(n_dims):
+            plt.subplot(n_dims, 1, i+1)
+            plt.plot(u_mid, u_dot2_min_all[:, i], 'b')
+            plt.plot(u_mid, u_dot2_max_all[:, i], 'r')
+            plt.title(f"u_dot2_{i}")
+
+        plt.figure()
+        for i in range(n_dims):
+            plt.subplot(n_dims, 1, i+1)
+            plt.plot(u_mid, a[:, i], 'k')
+            plt.plot(u_mid, a_minus[:, i], 'b')
+            plt.plot(u_mid, a_plus[:, i], 'r')
+            plt.title(f"a_{i}")
+
+    return u_dot2_min, u_dot2_max
+
+
+def constraints_v(p_prime, u, u_dot, u_dot2_min, u_dot2_max, v_abs_max=4.0, show=False):
+    n, n_dims = p_prime.shape
+    u_dot_min = np.zeros((n,))
+    u_dot_max = np.zeros((n,))
+
+    # v = np.zeros((n - 1, n_dims))
+    du = u[1:] - u[:-1]
+    u_dot_max_next = np.sqrt(np.maximum(u_dot[:-1]**2 + 2 * u_dot2_max * du, 0))
+    u_dot_min_next = np.sqrt(np.maximum(u_dot[:-1]**2 + 2 * u_dot2_min * du, 0))
+    u_dot_min_prev = np.sqrt(np.maximum(u_dot[1:]**2 - 2 * u_dot2_max * du, 0))
+    u_dot_max_prev = np.sqrt(np.maximum(u_dot[1:]**2 - 2 * u_dot2_min * du, 0))
+    u_dot_min[0] = u_dot_min_prev[0]
+    u_dot_min[-1] = u_dot_min_next[-1]
+    u_dot_max[0] = u_dot_max_prev[0]
+    u_dot_max[-1] = u_dot_max_next[-1]
+    u_dot_min[1:-1] = np.maximum(u_dot_min_next[:-1], u_dot_min_prev[1:])
+    u_dot_max[1:-1] = np.minimum(u_dot_max_next[:-1], u_dot_max_prev[1:])
+    u_dot_min = np.minimum(u_dot_min, u_dot_max)
+
+    p_prime_norm = np.linalg.norm(p_prime, axis=-1)
+    u_dot_min[:] = np.maximum(u_dot_min, 0)
+    u_dot_max[:] = np.minimum(u_dot_max, v_abs_max / p_prime_norm)
+
+    if show:
+        plt.figure()
+        plt.plot(u, u_dot, 'k')
+        plt.plot(u, u_dot_min, 'b')
+        plt.plot(u, u_dot_max, 'r')
+        plt.title("u_dot")
+
+    return u_dot_min, u_dot_max
+
+
+def main():
+    n = 256
+    # c = curves.Line1D(0, 4)
+    # c = curves.Line2D([0, 0], [4, 0])
+    # c = curves.CubicBezier([0, 0], [1, 0], [1, 2], [0, 0.7])
+    c = curves.CubicBezier([0, 0], [1, 0], [1, 1], [2, 1])
+    # c = curves.Circle2D(1.5)
+    u = np.linspace(0, 1, n)
+    u_dot = np.zeros((n,))
+    p = c(u)
+    p_prime = c.derivative(u)
+    p_prime2 = c.second_derivative(u)
+
+    u_dot[1:-1] = 1e-4
+    # u_dot[:] = 0.1*(2*u - 2*u*u)
+
+    # lambd = 0.1
+    lambd = 0.3
+    optimizing = True
+    max_itr = int(5e4)
+    k = 0
+    while optimizing:
+        show = k == -1
+        u_dot2_min, u_dot2_max = constraints_a(p_prime, p_prime2, u, u_dot, show=show)
+        u_dot_min, u_dot_max = constraints_v(p_prime, u, u_dot, u_dot2_min, u_dot2_max, show=show)
+
+        if show:
+            plt.show()
+            return
+
+        mse_diff = np.mean(np.square(u_dot[1:-1] - u_dot_max[1:-1]))
+        u_dot[1:-1] = lambd * u_dot_max[1:-1] + (1 - lambd) * u_dot[1:-1]
+        k += 1
+        optimizing = (mse_diff > 1e-10) and (k < max_itr)
+
+    np.save("curves/u.npy", u)
+    np.save("curves/u_dot.npy", u_dot)
+    np.save("curves/p.npy", p)
+    np.save("curves/p_prime.npy", p_prime)
+    np.save("curves/p_prime2.npy", p_prime2)
+
+    print(f"{k} steps")
+
+    u_mid = (u[:-1] + u[1:]) * 0.5
+
+    plt.figure()
+    plt.plot(u, u_dot, 'k')
+    plt.plot(u, u_dot_min, 'b')
+    plt.plot(u, u_dot_max, 'r')
+
+    plt.figure()
+    plt.plot(u_mid, u_dot2_min, 'b')
+    plt.plot(u_mid, u_dot2_max, 'r')
+
+    plt.show()
+
+
+if __name__ == "__main__":
+    main()