diff --git a/week4/README.MD b/week4/README.MD
new file mode 100644
index 0000000000000000000000000000000000000000..be5aeba2ed236a61a7e5a712e954b1f227d9fdf4
--- /dev/null
+++ b/week4/README.MD
@@ -0,0 +1,9 @@
+# Week 4 - Ordinary Differential Equations
+
+From 6.2, error at different step sizes for Euler integration (both are diverging) and Runge-Kutta with much smaller error.
+
+![](img/6_2_euler_v_runge.png)
+
+From 6.3, adaptive Runge-Kutta stepper's step size increasing to meet a desired error target. Could have used a more aggressive minimum error term to see the steps oscillate with the derivative instead of just meeting the requirements of the peak derivative.
+
+![](img/adaptive_runge_step_v_targetError.png)
diff --git a/week4/week4_2.py b/week4/week4_2.py
new file mode 100644
index 0000000000000000000000000000000000000000..9db6d6257a6070659631eea630414efd20bc4626
--- /dev/null
+++ b/week4/week4_2.py
@@ -0,0 +1,64 @@
+import numpy as np
+import matplotlib.pyplot as plt
+
+'''
+6.2)
+For a simple harmonic oscillator y'' + y = 0, with initial conditions y(0) = 1, y'(0) = 0,
+find y(t) from t = 0 to 100π. Use an Euler method and a fixed-step fourth-order Runge-Kutta method.
+For each method check how the average local error, and the error in the final value and slope, depend on the step size
+'''
+
+# Euler method
+def euler_integrate(step):
+    t = np.arange(0, 100*np.pi, step)
+    increments = np.arange(1, len(t), 1) # we will need something indexed from 1 to final time_step to not interfere with the initial conditions
+    y = np.zeros(len(t))
+    y[0] = 1
+    dy = np.zeros(len(t))
+    dy[0] = 0
+    for time in increments:
+        y[time] = y[time-1] + step*dy[time-1]
+        dy[time] = dy[time-1] - step*(y[time-1])
+        # dy[time] = dy[time-1] - step*(y[time]) # reverse euler? Just use the expected new y 
+    return y, dy, t
+
+# Runge-Kutta method
+def rk4_integrate(step):
+    t = np.arange(0, 100*np.pi, step)
+    increments = np.arange(1, len(t), 1) # we will need something indexed from 1 to final time_step to not interfere with the initial conditions
+    y = np.zeros(len(t))
+    y[0] = 1
+    dy = np.zeros(len(t))
+    dy[0] = 0
+    for time in increments:
+        k1, k2, k3, k4 = [0, 0], [0, 0], [0, 0], [0, 0] # make position and velocity vectors
+        
+        k1[0] = step * dy[time-1] # normal euler step
+        k1[1] = - step * y[time-1]
+        k2[0] = step * (dy[time-1] + k1[1]*0.5) # half euler step in the direction of k1
+        k2[1] = -step * (y[time-1] + k1[0]*0.5)
+        k3[0] = step * (dy[time-1] + k2[1]*0.5) # half euler step in the direction of k2
+        k3[1] = -step * (y[time-1] + k2[0]*0.5)
+        k4[0] = step * (dy[time-1] + k3[1]) # full euler step in the direction of k3
+        k4[1] = -step * (y[time-1] + k3[0])
+
+        y[time] = y[time-1] + (k1[0] + 2* k2[0] + 2 * k3[0] + k4[0])/6 # divide by 6 (not 4) because we assigned 2x value to k2 and k3
+        dy[time] = dy[time-1] + (k1[1] + 2* k2[1] + 2 * k3[1] + k4[1])/6
+    return y, dy, t
+
+step1 = 0.001
+step2 = 0.01
+
+# y1, dy1, t1 = euler_integrate(step1)
+# y2, dy2, t2 = euler_integrate(step2)
+
+y1, dy1, t1 = rk4_integrate(step1)
+y2, dy2, t2 = rk4_integrate(step2)
+
+fig, ax = plt.subplots(2)
+ax[0].plot(t1, y1)
+ax[0].title.set_text('Times step = ' + str(step1) + '\n Final y: ' + str(round(y1[-1],3)) + '\n Final dy: ' + str(round(dy1[-1],3)))
+ax[1].plot(t2, y2)
+ax[1].title.set_text('Times step = ' + str(step2) + '\n Final y: ' + str(round(y2[-1],3)) + '\n Final dy: ' + str(round(dy2[-1],3)))
+plt.tight_layout()
+plt.show()
\ No newline at end of file
diff --git a/week4/week4_3.py b/week4/week4_3.py
new file mode 100644
index 0000000000000000000000000000000000000000..c79fa9fd0ce2263144911bc64acd917f0d933d55
--- /dev/null
+++ b/week4/week4_3.py
@@ -0,0 +1,73 @@
+import numpy as np
+import matplotlib.pyplot as plt
+
+'''
+6.3)
+Write a fourth-order Runge Kutta adaptive stepper for the preceding problem,
+and check how the average step size that it finds depends on the desired local error.
+'''
+
+# Adaptive Runge-Kutta method (rewritten to be a step call)
+def rk4_step(step, y, dy):
+    k1, k2, k3, k4 = [0, 0], [0, 0], [0, 0], [0, 0] # make position and velocity vector
+    k1[0] = step * dy # normal euler step for velocity
+    k1[1] = - step * y  # normal euler step for position
+    k2[0] = step * (dy + k1[1]*0.5) # half euler step in the direction of k1
+    k2[1] = -step * (y + k1[0]*0.5)
+    k3[0] = step * (dy + k2[1]*0.5) # half euler step in the direction of k2
+    k3[1] = -step * (y + k2[0]*0.5)
+    k4[0] = step * (dy + k3[1]) # full euler step in the direction of k3
+    k4[1] = -step * (y + k3[0])
+    y_new = y + (k1[0] + 2*k2[0] + 2*k3[0] + k4[0])/6 # divide by 6 (not 4) because we assigned 2x value to k2 and k3
+    dy_new = dy + (k1[1] + 2*k2[1] + 2*k3[1] + k4[1])/6
+    return y_new, dy_new
+
+def rk4_adaptive(step_0, step_nudge, error_min, error_max):
+    ''' In this case we don't know how many timesteps we will take, so it's best to
+    start with lists and convert to numpy arrays at the end...
+    '''
+    t_final = 100*np.pi
+    t = [0]
+    y = [1]
+    dy = [0]
+    steps = [0.1]
+    
+    step = steps[0]
+    time = step
+    count = 1
+
+    while time < t_final:
+        y_full, dy_full = rk4_step(step, y[count-1], dy[count-1])
+        y_half1, dy_half1 = rk4_step(step*0.5, y[count-1], dy[count-1])
+        y_half2, dy_half2 = rk4_step(step*0.5, y_half1, dy_half1)
+
+        error = np.abs(y_full - y_half2) # error is how closely the full and half steps agree
+        # if we are too high or too low, nudge the step size and try again
+        if error > error_max:
+            step *= step_nudge
+        elif error < error_min:
+            step *= 1 - (step_nudge-1)
+        # otherwise go ahead and append and increment time and count
+        else:
+            y.append(y_full)
+            dy.append(dy_full)
+            steps.append(step)
+            time += step
+            t.append(time)
+            count += 1
+    return t, y, dy, steps
+
+step_0 = 0.1
+nudge = 0.99 # increase or decrease the step by this percentage to nudge the step size
+error_min =  1e-6 # easier to choose if theses are informed by something physical
+error_max = 1e-4
+
+t, y, dy, steps = rk4_adaptive(step_0, nudge, error_min, error_max)
+
+fig, ax = plt.subplots(2)
+ax[0].plot(t, y)
+ax[0].title.set_text('\n Max Error: ' + str(round(error_max, 10)) + '\n Min Error: ' + str(round(error_min, 10)))
+ax[1].plot(t, steps)
+ax[1].title.set_text('Total number of steps: ' + str(len(t)) + '\n Average step size: ' + str(round(np.mean(steps), 6)))
+plt.tight_layout()
+plt.show()
diff --git a/week4/week4_4.py b/week4/week4_4.py
new file mode 100644
index 0000000000000000000000000000000000000000..41b64b38ffcebfa14179fc4ebbb1817e8c0f74d0
--- /dev/null
+++ b/week4/week4_4.py
@@ -0,0 +1,65 @@
+import numpy as np
+import matplotlib.pyplot as plt
+
+'''
+6.4)
+Numerically solve the differential equation found in Problem 4.3:
+l*ddθ + (g + ddz)*sinθ = 0
+Take the motion of the platform to be periodic, and interactively explore the dynamics of
+the pendulum as a function of the amplitude and frequency of the excitation.
+
+Consider a pendulum with a mass m connected by a massless rod of length l to a moveable platform at height z (Figure 6.4)
+'''
+step = 0.001
+t_final = 10*np.pi
+t = np.arange(0, t_final, step)
+increments = np.arange(1, len(t), 1)
+time = 0
+
+# Platform Parameters (periodic motion and very differentiable)
+A = 1 # amplitude of the platform's motion [m]
+platform_omega = 1 # angular frequency of the platform's motion [rad/s]
+zs = A*np.sin(platform_omega*t) # all platform positions as a function of time
+ddzs = A*platform_omega**2*np.cos(platform_omega*t) # all platform accelerations as a function of time
+
+# Pendulum Parameters
+thetas = np.zeros(len(t))
+d_thetas = np.zeros(len(t))
+dd_thetas = np.zeros(len(t))
+l = 1 # length of pendulum
+g = 9.81 # acceleration due to gravity
+theta_0 = 0 # initial angle of the pendulum [rad]
+d_theta_0 = 1 # initial angular velocity of the pendulum [rad/s]
+
+def d_theta(theta, omega, t):
+    return omega
+
+def dd_theta(theta, omega, t):
+    solution = -np.sin(theta)/l * (g - ddzs[int(t/step)])
+    print(solution)
+    return solution
+
+def rk4_step(prev_values, dif_eqs, step, time):
+    variables = np.array(prev_values)
+    k1, k2, k3, k4 = [0, 0], [0, 0], [0, 0], [0, 0] # make position and velocity vector
+    ddy = dif_eqs[1](prev_values[0], prev_values[1], time)
+    dy = dif_eqs[0](prev_values[0], prev_values[1], time)
+    print(ddy)
+    k1[0] = step * ddy # normal euler step for velocity
+    k1[1] = - step * dy  # normal euler step for position
+    k2[0] = step * (ddy + k1[1]*0.5) # half euler step in the direction of k1
+    k2[1] = -step * (dy + k1[0]*0.5)
+    k3[0] = step * (ddy + k2[1]*0.5) # half euler step in the direction of k2
+    k3[1] = -step * (dy + k2[0]*0.5)
+    k4[0] = step * (ddy + k3[1]) # full euler step in the direction of k3
+    k4[1] = -step * (dy + k3[0])
+    variables = variables + np.array(k1)/6 + np.array(k2)/3 + np.array(k3)/3 + np.array(k4)/6
+    return variables
+
+for increment in increments:
+    time = t[increment]
+    thetas[increment], d_theta[increment] = rk4_step([thetas[increment-1], d_thetas[increment-1]], [d_theta, dd_theta], step, time)
+
+# let's plot the chaotic theta over the periodic z
+plt.plot(t, zs)
+plt.plot(t, thetas)
\ No newline at end of file