# dy/dt = func(t,y)
# y = [q,p]   
# Hamiltonian system H(q,p) = p^2/2 + V(q,parms)
# V(q) is potential 
# Vprime(q,parms) is dV/dq(q,parms)
def func(t,y,parms):
    q = y[0]
    p = y[1]
    dydt = np.array([p,-Vprime(q,parms)])
    return dydt
# dull Harmonic oscillator!  dH/dq = dV/dq = q
def Vprime(q,parms):
    return q
# two-stage second-order Runge–Kutta method known as Ralston method
# integrate a single step of duration dt
# the function func() returns dy/dt and uses parameters parms
def runge_s2(func,t,y,dt,parms):
    dt23 = 2.0*dt/3.0 # 2/3rd of a step
    #print(dt23)
    k1 = func(t,y,parms)  # returns dy/dt
    #print(k1)
    k2 = func(t + dt23,y + dt23*k1,parms)
    ynew = y + dt*(k1/4. + k2*3./4.)
    return ynew    # return new value of y at end of timestep 
# second order leapfrog method for a Hamiltonian system y = [q,p]
def leap_frog(func,t,y,dt,parms):
    q_n = y[0]
    p_n = y[1]
    qhalf = q_n + p_n*dt/2  # half step of drift 
    yhalf = np.array([qhalf,p_n])  # evalulate dydt at half step
    dydt_half = func(t,yhalf,parms)
    pn1 = p_n +  dydt_half[1]*dt  # full step of kick
    qn1 = qhalf + pn1*dt/2   # another half step of kick 
    ynew = np.array([qn1,pn1])
    return ynew  # return new value of y at end of timestep 
# integrate nn steps 
# initial condition is y0 at time t0
# parms is passed to function func
# func returns dy/dt
# i_type determine which integrator is called
# returned are arrays of q,p,t values
def nsteps(func,y0,t0,dt,nn,parms,i_type):
    qarr = []  #allocate some arrays
    parr = []
    tarr = []
    y = y0  # set initial condition 
    t = t0
    qarr = np.append(qarr,y[0])  # store initial condition 
    parr = np.append(parr,y[1])
    tarr = np.append(tarr,t)
    for i in range(nn):
        if (i_type == 'rungeKutta'):
            ynew = runge_s2(func,t,y,dt,parms)  # integrate with runge kutte second order
        else:
            ynew = leap_frog(func,t,y,dt,parms)  # integrate with leapfrog integrator
        y = ynew 
        t += dt
        qarr = np.append(qarr,y[0])  # append integrated values to arrays
        parr = np.append(parr,y[1])
        tarr = np.append(tarr,t)
        
    return qarr,parr,tarr # return arrays