Finding an Optimal Trajectory: The Sliding Particle

Finding an Optimal Trajectory: The Sliding Particle#


Setup

  • consider a particle that

    • starts at \(\vec{r}(t=0) = [0,a]\) with \(v(t=0) = v_0\)

    • slides towards \(\vec{r}(t=T) = [1,b]\)

    • following the path given by \(\vec{p}(x) = [x, f(x)]\) with \(x \in [0,1]\)

  • assume \(mg=1\):

  • \(\Rightarrow\) at position \([x,f(x)]\), its speed \(v(x)\) satisfies: \(\frac12 v(x)^2 = \frac12 v_0^2 + [a-f(x)]\) (initial kinetic + potential energy)

  • \(\Rightarrow v(x) = \sqrt{v_0^2 + 2[a-f(x)]}\)

  • direction of \(\vec{v}\) will follow the path given by \(\vec{r}(x) = [x, f(x)]\), i.e. \(\vec{v} \propto \frac{\partial \vec{r}(x)}{\partial x} = [1, f'(x)]\)

  • \(\Rightarrow\) normalized horizontal speed \(v_h(x) = \frac{v(x)}{\sqrt{1+f'(x)^2}} = \frac{\sqrt{v_0^2 + 2[a-f(x)]}}{\sqrt{1+f'(x)^2}}.\)

\[\large \Rightarrow T = \int_0^1 \frac{dx}{v_h(x)} = \int_0^1 \frac{\sqrt{1+f'(x)^2}}{\sqrt{v_0^2 + 2[a-f(x)]}}\,dx\]

Find \(f(x)\) for a given \(v_0\) so that \(T\) is minimized.

Algorithm#

Setup

  • divide interval \(x \in [0,1]\) into \(n\) subintervals: \(x_i= i/n\), for \(i=0,\ldots, n\)

  • assume that \(f(x)\) is linear on each interval \([x_i, x_{i+1}]\), and continuous on the entire interval \([0,1]\)

  • \(\Rightarrow\) \(f(x)\) is determined by its value on the points \(x_i\): \(f_i = f(x_i)\)

  • \(\Rightarrow\) optimize \(T\) over all such functions

  • Note 1: \(f_0=a\) and \(f_n=b\), since we start at \((0,a)\) and end up at \((1,b)\)

  • Note 2: \(f\) has a constant derivative on the interval \((x_i, x_{i+1})\): \(f(x)|_{x\in (x_i, x_{i+1})} \approx f_i + f'_i \, (x - x_i)\) $\( \Rightarrow T = \sum_{i=0}^{n-1} \sqrt{1+{f_i'}^2} \int_{x_i}^{x_{i+1}} \frac{dx}{\sqrt{v_0^2 + 2a-2f_i-2f_i'(x-x_i)}}\)$

  • the integral can now be calculated exactly yielding the functional: $\( \Rightarrow T[f'] = -\sum_{i=0}^{n-1} \frac{\sqrt{1+{f'_i}^2}}{f'_i}\, \left( \sqrt{v_0^2 + 2(a-f_{i+1})} - \sqrt{v_0^2 + 2(a-f_i)}\right)\)$

  • \(\Rightarrow\) minimize this functional to get optimal \(f'_i\)

  • \(\Rightarrow\) from this we can easily re-construct \(f_i = a + \sum_{j\le i}f'_j x_j\)

Be careful: For \(v_0=0\), the exact solution \(f\) to the problem is given by a cycloid, which has an infinite derivative at \(x=0\). This leads to large inaccuracies when trying to interpolate the integral.

import numpy as np
import matplotlib.pyplot as plt
import scipy.optimize as optimize
# settings
n  = 75
a  = 0.4  # starting height
b  = 0.0  # ending height 
v0 = 0.0  # starting velocity, in the direction of 
          # the trajectory
dx = 1.0/(n-1)

def Tf(df, v0):
    
    v2 = v0**2
    T  = 0
    
    for i in range(0, len(df)):
        
        v2new = v2 - 2*df[i]*dx
        
        # in case we found a negative (unphysical)
        # new velocity squared we return a huge cost
        # such that the algorithm avoids this "solution"
        if(v2new < 0):
            return 10**10
        
        if(df[i] >= 10**(-9)):
            
            T += np.sqrt( 1 + df[i]**2 ) / df[i] \
                 * ( np.sqrt(v2) - np.sqrt(v2new) )
            
        else: 
            
            # be careful with very small df[i]
            T += np.sqrt( 1 + df[i]** 2) \
                 / ( np.sqrt(v2) + np.sqrt(v2new) ) \
                 * 2.0 * dx
                
        # save new v^2 as old one
        v2 = v2new
        
    return T
    
# define equality constraint, so that only solutions
# are chosen which end up at (1, b)
def h1(df):
    return sum(df)*dx-(b-a)

cons = ({'type':'eq', 
         'fun': h1})

# optimize it
sol = optimize.minimize(Tf, x0=[b-a]*(n-1), method='SLSQP', 
                        constraints=cons, args=v0)

print( Tf(sol.x, v0) )

# plot it
plt.figure(1)
plt.grid()
x = np.linspace(0, 1, n)
plt.plot(x,a+dx*np.cumsum(np.insert(sol.x,0,0)))
plt.show()
1.8136795180800998
../_images/75e975d9497750086d025b0fe80902daa163813c3e8d64bcd5018e614d330e32.png

Plot it for varying initial velocities:

plt.figure(1)
plt.grid()

for v0 in np.linspace(0, 3, 5):

    sol = optimize.minimize(Tf, x0=[b-a]*(n-1), method='SLSQP', 
                            constraints=cons, args=v0)

    plt.plot(x, a+dx*np.cumsum(np.insert(sol.x,0,0)), label=v0)
    
plt.legend()
plt.show()
../_images/c89ca465d58bd3845b1cd3e5fda4220f604e2728f667b9c130ea294b0c20b64d.png

What happens when we remove the constraint?

plt.figure(1)
plt.grid()

for v0 in np.linspace(0, 3, 5):

    sol = optimize.minimize(Tf, x0=[b-a]*(n-1), method='SLSQP', 
                            args=v0)

    plt.plot(x, a+dx*np.cumsum(np.insert(sol.x,0,0)), label=v0)
    
plt.legend()
plt.show()
../_images/7ac11e9ab361e412bdb9f993aa51e544692f882d6b1eedfa1bb0c79729f93f75.png
%%html
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">I built a BRACHISTOCHRONE with <a href="https://twitter.com/tweetsauce?ref_src=twsrc%5Etfw">@tweetsauce</a>! Video: <a href="https://t.co/rC5oktUATG">https://t.co/rC5oktUATG</a> <a href="https://twitter.com/braincandylive?ref_src=twsrc%5Etfw">@braincandylive</a> <a href="https://t.co/sj1FVgqf5v">pic.twitter.com/sj1FVgqf5v</a></p>&mdash; Adam Savage (@donttrythis) <a href="https://twitter.com/donttrythis/status/823257178727321600?ref_src=twsrc%5Etfw">January 22, 2017</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

With Friction#

  • friction: force opposite to the speed of the particle, with magnitude \(\gamma \cdot v\)

  • differentiated energy equation (measures the change in kinetic energy) becomes

\[\begin{split} \begin{align} \frac{d\left(\frac12v(x)^2\right)}{dx} &= -f'(x) -\gamma v(x)\sqrt{1+f'(x)^2} \\ \Leftrightarrow v_{i+1}^2 &= v_i - 2 f'_i dx - 2 \gamma v_i \sqrt{1+f_i'^2} dx \end{align} \end{split}\]
# settings
n  = 75
a  = 0.1  # starting height
b  = 0.0  # ending height (should be < a + 0.5*v^2, because g=1)
dx = 1.0/(n-1)

v0 = 1.5  # 0.9 # starting velocity, in the direction of the trajectory
g  = 0.5  # 1.5 # friction constant

def TfFriction(df, v0, g):
    v2 = v0**2
    T  = 0
    for i in range(0,len(df)):
        
        # Euler step
        v2new = v2 - 2*df[i]*dx - 2*g*np.sqrt(v2)*np.sqrt(1+df[i]**2)*dx
        
        # in case we found a negative new velocity squared
        if(v2new < 0):
            return 10**10
        
        # T calculation
        if(df[i] >= 10**(-9)):
            T += np.sqrt( 1 + df[i]**2 ) / df[i] * ( np.sqrt(v2) - np.sqrt(v2new) )
        else: # be careful with very small df[i]
            T += np.sqrt( 1 + df[i]** 2) / ( np.sqrt(v2) + np.sqrt(v2new) ) * 2.0 * dx
                
        # save new v^2 as old one
        v2 = v2new
        
    return T
    
# define equality constraint, so that only solutions
# are chosen which end up at (1, b)
def h1(df):
    return sum(df)*dx-(b-a)
cons=({'type':'eq', 'fun': h1})

# get optimized solution with and without friction
solFriction = optimize.minimize(TfFriction, x0=[b-a]*(n-1), method='SLSQP', constraints=cons, args=(v0, g))

# plot it
plt.figure(1)
x = np.linspace(0, 1, n)
plt.plot(x,a+dx*np.cumsum(np.insert(solFriction.x, 0, 0)), label='with friction')
plt.legend()
plt.show()
# ... linear parts are prefered
../_images/e54eb0f752885208989c2d0b45f7aed3616939f3ff35df822220dbbdef9d4303.png
plt.figure(1)

# vary gamma
for g in np.linspace(0, 1.5, 4):
    solFriction = optimize.minimize(TfFriction, x0=[b-a]*(n-1), method='SLSQP', constraints=cons, args=(v0, g))
    plt.plot(x, a+dx*np.cumsum(np.insert(solFriction.x, 0, 0)), label="$\gamma = %3.2f$"%g)
    
plt.legend()
plt.show()
<>:6: SyntaxWarning: "\g" is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\g"? A raw string is also an option.
<>:6: SyntaxWarning: "\g" is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\g"? A raw string is also an option.
/tmp/ipykernel_597/2299117506.py:6: SyntaxWarning: "\g" is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\g"? A raw string is also an option.
  plt.plot(x, a+dx*np.cumsum(np.insert(solFriction.x, 0, 0)), label="$\gamma = %3.2f$"%g)
../_images/25e4a9861c1861cda4c6c9979e8c2728580601daacd2c1f9ccbe3f349c524186.png