Exercise 1.44: The double pendulum

This namespace explores Exercise 1.44 from Sussman and Wisdom's Structure and Interpretation of Classical Mechanics, using the SICMUtils Clojure library and the Clerk rendering environment.

(ns sicmutils
(:refer-clojure
:exclude [+ - * / partial ref zero? numerator denominator compare = run!])
(:require [nextjournal.clerk :as clerk]
[sicmutils.env :as e :refer :all]
[sicmutils.expression.render :as xr]))

Lagrangian

Start with a coordinate transformation from theta1, theta2 to rectangular coordinates. We'll generate our Lagrangian by composing this with an rectangular Lagrangian with the familiar form of T - V.

(defn angles->rect [l1 l2]
(fn [[_ [theta1 theta2]]]
(let [x1 (* l1 (sin theta1))
y1 (- (* l1 (cos theta1)))
x2 (+ x1 (* l2 (sin (+ theta1 theta2))))
y2 (- y1 (* l2 (cos (+ theta1 theta2))))]
(up x1 y1 x2 y2))))
#object[sicmutils$angles__GT_rect 0x726bad51 "
sicmutils$angles__GT_rect@726bad51"
]

T describes the sum of the kinetic energy of two particles in rectangular coordinates.

(defn T [m1 m2]
(fn [[_ _ [xdot1 ydot1 xdot2 ydot2]]]
(+ (* 1/2 m1 (+ (square xdot1)
(square ydot1)))
(* 1/2 m2 (+ (square xdot2)
(square ydot2))))))
#object[sicmutils$T 0xb308aa8 "
sicmutils$T@b308aa8"
]

V describes a uniform gravitational potential with coefficient g, acting on two particles with masses of, respectively, m1 and m2. Again, this is written in rectangular coordinates.

(defn V [m1 m2 g]
(fn [[_ [_ y1 _ y2]]]
(+ (* m1 g y1)
(* m2 g y2))))
#object[sicmutils$V 0x7647d336 "
sicmutils$V@7647d336"
]

Form the rectangular Lagrangian L by subtracting (V m1 m2 g) from (T m1 m2):

(defn L-rect [m1 m2 g]
(- (T m1 m2)
(V m1 m2 g)))
#object[sicmutils$L_rect 0x69dca59d "
sicmutils$L_rect@69dca59d"
]

Form the final Langrangian in generalized coordinates (the angles of each segment) by composing L-rect with a properly transformed angles->rect coordinate transform!

(defn L-double-pendulum [m1 m2 l1 l2 g]
(compose (L-rect m1 m2 g)
(F->C
(angles->rect l1 l2))))
#object[sicmutils$L_double_pendulum 0x7ef33bda "
sicmutils$L_double_pendulum@7ef33bda"
]

The Lagrangian is big and hairy:

(def symbolic-L
((L-double-pendulum 'm_1 'm_2 'l_1 'l_2 'g)
(up 't
(up 'theta_1 'theta_2)
(up 'theta_1dot 'theta_2dot))))
(- (+ (* 0.5 m_1 (+ (expt (* l_1 (cos theta_1) theta_1dot) 2) (expt (* -1 l_1 (- (sin theta_1)) theta_1dot) 2))) (* 0.5 m_2 (+ (expt (+ (* l_1 (cos theta_1) theta_1dot) (* l_2 (cos (+ theta_1 theta_2)) (+ theta_1dot theta_2dot))) 2) (expt (+ (* -1 l_1 (- (sin theta_1)) theta_1dot) (* -1 l_2 (- (sin (+ theta_1 theta_2))) (+ theta_1dot theta_2dot))) 2)))) (+ (* m_1 g (- (* l_1 (cos theta_1)))) (* m_2 g (- (- (* l_1 (cos theta_1))) (* l_2 (cos (+ theta_1 theta_2)))))))

Let's simplify that:

(simplify symbolic-L)
(+ (* l_1 l_2 m_2 (expt theta_1dot 2) (cos theta_1) (cos (+ theta_1 theta_2))) (* l_1 l_2 m_2 (expt theta_1dot 2) (sin theta_1) (sin (+ theta_1 theta_2))) (* l_1 l_2 m_2 theta_1dot theta_2dot (cos theta_1) (cos (+ theta_1 theta_2))) (* l_1 l_2 m_2 theta_1dot theta_2dot (sin theta_1) (sin (+ theta_1 theta_2))) (* 0.5 (expt l_1 2) m_1 (expt theta_1dot 2)) (* 0.5 (expt l_1 2) m_2 (expt theta_1dot 2)) (* 0.5 (expt l_2 2) m_2 (expt theta_1dot 2)) (* (expt l_2 2) m_2 theta_1dot theta_2dot) (* 0.5 (expt l_2 2) m_2 (expt theta_2dot 2)) (* g l_1 m_1 (cos theta_1)) (* g l_1 m_2 (cos theta_1)) (* g l_2 m_2 (cos (+ theta_1 theta_2))))

Better yet, let's render it as LaTeX, and create a helper function, render-eq to make it easier to render simplified equations:

(def render-eq
(comp clerk/tex ->TeX simplify))
#object[clojure.core$comp$fn__5825 0x46728cd2 "
clojure.core$comp$fn__5825@46728cd2"
]
(render-eq symbolic-L)
l1l2m2θ1˙2cos(θ1)cos(θ1+θ2)+l1l2m2θ1˙2sin(θ1)sin(θ1+θ2)+l1l2m2θ1˙θ2˙cos(θ1)cos(θ1+θ2)+l1l2m2θ1˙θ2˙sin(θ1)sin(θ1+θ2)+12l12m1θ1˙2+12l12m2θ1˙2+12l22m2θ1˙2+l22m2θ1˙θ2˙+12l22m2θ2˙2+gl1m1cos(θ1)+gl1m2cos(θ1)+gl2m2cos(θ1+θ2)l_1\,l_2\,m_2\,{{\theta}_{\dot 1}}^{2}\,\cos\left({\theta}_1\right)\,\cos\left({\theta}_1 + {\theta}_2\right) + l_1\,l_2\,m_2\,{{\theta}_{\dot 1}}^{2}\,\sin\left({\theta}_1\right)\,\sin\left({\theta}_1 + {\theta}_2\right) + l_1\,l_2\,m_2\,{\theta}_{\dot 1}\,{\theta}_{\dot 2}\,\cos\left({\theta}_1\right)\,\cos\left({\theta}_1 + {\theta}_2\right) + l_1\,l_2\,m_2\,{\theta}_{\dot 1}\,{\theta}_{\dot 2}\,\sin\left({\theta}_1\right)\,\sin\left({\theta}_1 + {\theta}_2\right) + \frac{1}{2}\,{l_1}^{2}\,m_1\,{{\theta}_{\dot 1}}^{2} + \frac{1}{2}\,{l_1}^{2}\,m_2\,{{\theta}_{\dot 1}}^{2} + \frac{1}{2}\,{l_2}^{2}\,m_2\,{{\theta}_{\dot 1}}^{2} + {l_2}^{2}\,m_2\,{\theta}_{\dot 1}\,{\theta}_{\dot 2} + \frac{1}{2}\,{l_2}^{2}\,m_2\,{{\theta}_{\dot 2}}^{2} + g\,l_1\,m_1\,\cos\left({\theta}_1\right) + g\,l_1\,m_2\,\cos\left({\theta}_1\right) + g\,l_2\,m_2\,\cos\left({\theta}_1 + {\theta}_2\right)

And here are the equations of motion for the system:

(let [L (L-double-pendulum 'm_1 'm_2 'l_1 'l_2 'g)]
(binding [xr/*TeX-vertical-down-tuples* true]
(render-eq
(((Lagrange-equations L)
(up (literal-function 'theta_1)
(literal-function 'theta_2)))
't))))
[2l1l2m2cos(θ1(t))Dθ1(t)Dθ2(t)sin(θ1(t)+θ2(t))l1l2m2cos(θ1(t))(Dθ2(t))2sin(θ1(t)+θ2(t))+2l1l2m2Dθ1(t)Dθ2(t)sin(θ1(t))cos(θ1(t)+θ2(t))+l1l2m2(Dθ2(t))2sin(θ1(t))cos(θ1(t)+θ2(t))+2l1l2m2cos(θ1(t))cos(θ1(t)+θ2(t))D2θ1(t)+l1l2m2cos(θ1(t))cos(θ1(t)+θ2(t))D2θ2(t)+2l1l2m2sin(θ1(t)+θ2(t))sin(θ1(t))D2θ1(t)+l1l2m2sin(θ1(t)+θ2(t))sin(θ1(t))D2θ2(t)+gl1m1sin(θ1(t))+gl1m2sin(θ1(t))+gl2m2sin(θ1(t)+θ2(t))+l12m1D2θ1(t)+l12m2D2θ1(t)+l22m2D2θ1(t)+l22m2D2θ2(t)l1l2m2cos(θ1(t))(Dθ1(t))2sin(θ1(t)+θ2(t))l1l2m2(Dθ1(t))2sin(θ1(t))cos(θ1(t)+θ2(t))+l1l2m2cos(θ1(t))cos(θ1(t)+θ2(t))D2θ1(t)+l1l2m2sin(θ1(t)+θ2(t))sin(θ1(t))D2θ1(t)+gl2m2sin(θ1(t)+θ2(t))+l22m2D2θ1(t)+l22m2D2θ2(t)]\begin{bmatrix}\displaystyle{-2\,l_1\,l_2\,m_2\,\cos\left({\theta}_1\left(t\right)\right)\,D{\theta}_1\left(t\right)\,D{\theta}_2\left(t\right)\,\sin\left({\theta}_1\left(t\right) + {\theta}_2\left(t\right)\right) - l_1\,l_2\,m_2\,\cos\left({\theta}_1\left(t\right)\right)\,{\left(D{\theta}_2\left(t\right)\right)}^{2}\,\sin\left({\theta}_1\left(t\right) + {\theta}_2\left(t\right)\right) + 2\,l_1\,l_2\,m_2\,D{\theta}_1\left(t\right)\,D{\theta}_2\left(t\right)\,\sin\left({\theta}_1\left(t\right)\right)\,\cos\left({\theta}_1\left(t\right) + {\theta}_2\left(t\right)\right) + l_1\,l_2\,m_2\,{\left(D{\theta}_2\left(t\right)\right)}^{2}\,\sin\left({\theta}_1\left(t\right)\right)\,\cos\left({\theta}_1\left(t\right) + {\theta}_2\left(t\right)\right) + 2\,l_1\,l_2\,m_2\,\cos\left({\theta}_1\left(t\right)\right)\,\cos\left({\theta}_1\left(t\right) + {\theta}_2\left(t\right)\right)\,{D}^{2}{\theta}_1\left(t\right) + l_1\,l_2\,m_2\,\cos\left({\theta}_1\left(t\right)\right)\,\cos\left({\theta}_1\left(t\right) + {\theta}_2\left(t\right)\right)\,{D}^{2}{\theta}_2\left(t\right) + 2\,l_1\,l_2\,m_2\,\sin\left({\theta}_1\left(t\right) + {\theta}_2\left(t\right)\right)\,\sin\left({\theta}_1\left(t\right)\right)\,{D}^{2}{\theta}_1\left(t\right) + l_1\,l_2\,m_2\,\sin\left({\theta}_1\left(t\right) + {\theta}_2\left(t\right)\right)\,\sin\left({\theta}_1\left(t\right)\right)\,{D}^{2}{\theta}_2\left(t\right) + g\,l_1\,m_1\,\sin\left({\theta}_1\left(t\right)\right) + g\,l_1\,m_2\,\sin\left({\theta}_1\left(t\right)\right) + g\,l_2\,m_2\,\sin\left({\theta}_1\left(t\right) + {\theta}_2\left(t\right)\right) + {l_1}^{2}\,m_1\,{D}^{2}{\theta}_1\left(t\right) + {l_1}^{2}\,m_2\,{D}^{2}{\theta}_1\left(t\right) + {l_2}^{2}\,m_2\,{D}^{2}{\theta}_1\left(t\right) + {l_2}^{2}\,m_2\,{D}^{2}{\theta}_2\left(t\right)} \cr \cr \displaystyle{l_1\,l_2\,m_2\,\cos\left({\theta}_1\left(t\right)\right)\,{\left(D{\theta}_1\left(t\right)\right)}^{2}\,\sin\left({\theta}_1\left(t\right) + {\theta}_2\left(t\right)\right) - l_1\,l_2\,m_2\,{\left(D{\theta}_1\left(t\right)\right)}^{2}\,\sin\left({\theta}_1\left(t\right)\right)\,\cos\left({\theta}_1\left(t\right) + {\theta}_2\left(t\right)\right) + l_1\,l_2\,m_2\,\cos\left({\theta}_1\left(t\right)\right)\,\cos\left({\theta}_1\left(t\right) + {\theta}_2\left(t\right)\right)\,{D}^{2}{\theta}_1\left(t\right) + l_1\,l_2\,m_2\,\sin\left({\theta}_1\left(t\right) + {\theta}_2\left(t\right)\right)\,\sin\left({\theta}_1\left(t\right)\right)\,{D}^{2}{\theta}_1\left(t\right) + g\,l_2\,m_2\,\sin\left({\theta}_1\left(t\right) + {\theta}_2\left(t\right)\right) + {l_2}^{2}\,m_2\,{D}^{2}{\theta}_1\left(t\right) + {l_2}^{2}\,m_2\,{D}^{2}{\theta}_2\left(t\right)}\end{bmatrix}

What do these mean?

  • the system has two degrees of freedom: θ1\theta_1 and θ2\theta_2.
  • at any point t in time, the two equations above, full of first and second
  • order derivatives of the position functions, will stay true
  • the system can use these equations to simulate the system, one tick at a time.

Simulation

Next, let's run a simulation using those equations of motion and collect data on each coordinate's evolution.

Here are the constants specified in exercise 1.44:

masses in kg:

(def m1 1.0)
1
(def m2 3.0)
3

lengths in meters:

(def l1 1.0)
1
(def l2 0.9)
0.9

g in units of m/s^2:

(def g 9.8)
9.8

And two sets of initial pairs of theta1, theta2 angles corresponding to chaotic and regular initial conditions:

(def chaotic-initial-q (up (/ Math/PI 2) Math/PI))
(1.5707963267948966 3.141592653589793)
(def regular-initial-q (up (/ Math/PI 2) 0.0))
(1.5707963267948966 0)

Composing Lagrangian->state-derivative with L-double-pendulum produces a state derivative that we can use with our ODE solver:

(def state-derivative
(compose
Lagrangian->state-derivative
L-double-pendulum))
#object[clojure.lang.AFunction$1 0x49183b7e "
clojure.lang.AFunction$1@49183b7e"
]

Finally, two default parameters for our simulation. We'll record data in steps of 0.01 seconds, and simulate to a horizon of 50 seconds.

(def step 0.01)
0.01
(def horizon 50)
50

run! will return a sequence of 5001 states, one for each measured point in the simulation. The smaller-arity version simply passes in default masses and lengths, but you can override those with the larger arity version if you like.

(The interface here could use some work: integrate-state-derivative tidies this up a bit, but I want it out in the open for now.)

(defn run!
([step horizon initial-coords]
(run! step horizon l1 l2 m1 m2 g initial-coords))
([step horizon l1 l2 m1 m2 g initial-coords]
(let [collector (atom (transient []))
initial-state (up 0.0
initial-coords
(up 0.0 0.0))]
((evolve state-derivative m1 m2 l1 l2 g)
initial-state
step
horizon
{:compile? true
:epsilon 1.0e-13
:observe (fn [_ state]
(swap!
collector conj! state))})
(persistent! @collector))))
#object[sicmutils$run_BANG_ 0x10e9f08 "
sicmutils$run_BANG_@10e9f08"
]

Run the simulation for each set of initial conditions and show the final state. Chaotic first:

(def raw-chaotic-data
(run! step horizon chaotic-initial-q))
[(0 (1.5707963267948966 3.141592653589793) (0 0)) (0.009999999999999998 (1.5703063268694102 3.14208265361986) (-0.09799995529340401 0.09800001803950574)) (0.019999999999999993 (1.5688363315635692 3.143552655513996) (-0.19599856940634308 0.19600057725813616)) (0.03 (1.5663863811113454 3.146002675507077) (-0.2939891369903126 0.2940043833599193)) (0.04000000000000001 (1.562956631954858 3.149432776726657) (-0.3919542297291326 0.39201846924143363)) (0.05000000000000001 (1.558547490683088 3.1538431232488406) (-0.48986036114525866 0.49005634949325527)) (0.06000000000000002 (1.5531598010356917 3.159234055599955) (-0.5876527189011422 0.5881401515558404)) (0.06999999999999995 (1.546795082923507 3.165606187341447) (-0.6852500466002256 0.6863026951827188)) (0.07999999999999993 (1.5394558213585265 3.1729605220096277) (-0.7825398067912548 0.7845894747326742)) (0.08999999999999993 (1.5311458015778776 3.181298589127045) (-0.8793738164107565 0.8830604783845343)) (0.09999999999999994 (1.5218704844072315 3.190622597231871) (-0.9755646118932262 0.9817917559202739)) (0.10999999999999993 (1.511637413000679 3.2009356008752636) (-1.0708828683338556 1.0808766242569914)) (0.1199999999999999 (1.5004566385363898 3.2122416773262374) (-1.165056257553007 1.1804263803858146)) (0.1299999999999998 (1.4883411483655926 3.2245461073501347) (-1.257770172942257 1.280570378803589)) (0.13999999999999985 (1.4753072757380539 3.2378555529811224) (-1.3486707614227982 1.3814553297600172)) (0.14999999999999986 (1.461375065947878 3.2521782238391364) (-1.4373706697588804 1.4832436911030527)) (0.15999999999999986 (1.4465685700778879 3.267524022441586) (-1.523457819407898 1.5861110652399182)) (0.16999999999999987 (1.4309160351401522 3.2839046583769047) (-1.6065073596759356 1.6902425773942904)) (0.17999999999999988 (1.4144499590493442 3.30133372140343) (-1.6860967092587766 1.7958283025307598)) (0.1899999999999999 (1.397206981251419 3.319826704763722) (-1.7618232889614982 1.9030579220696064)) 4981 more elided]

Looks good:

(peek raw-chaotic-data)
(49.99999999999393 (-0.7253216905180996 27.204660120055955) (2.630986320591619 -5.649480294737957))

Next, the regular initial condition:

(def raw-regular-data
(run! step horizon regular-initial-q))
[(0 (1.5707963267948966 0) (0 0)) (0.010000000000000004 (1.5703063268694089 0.0004899998209110522) (-0.09799995529342519 0.09799989254735403)) (0.020000000000000004 (1.5688363315635574 0.001959988538482646) (-0.19599856941148464 0.19599656156730913)) (0.03 (1.5663863811107515 0.0044098694512942896) (-0.29398913718793995 0.29397389111112554)) (0.040000000000000015 (1.5629566319443307 0.007839266569336433) (-0.39195423235948346 0.39188999674445024)) (0.050000000000000024 (1.5585474905851686 0.012247202807576447) (-0.4898603807158662 0.4896644213695836)) (0.06000000000000002 (1.553159800430448 0.01763165101050902) (-0.587652819630181 0.5871655362912933)) (0.07000000000000002 (1.5467950801044628 0.023988960989759833) (-0.6852504482852072 0.6841983954406228)) (0.08000000000000003 (1.539455810691188 0.031313168944708494) (-0.7825411344032203 0.7804934369649791)) (0.09000000000000001 (1.5311457671636481 0.03959520040392489) (-0.8793776135754071 0.8756965939174625)) (0.1 (1.5218703866044474 0.04882198430240192) (-0.9755742884739586 0.9693615460953742)) (0.11000000000000013 (1.51163716242398 0.05897550388894877) (-1.0709052950931244 1.0609449896378502)) (0.12000000000000012 (1.5004560499204507 0.07003181939656877) (-1.1651042351860939 1.1498058811620278)) (0.13000000000000014 (1.4883398645621793 0.08196010694002998) (-1.2578659587648564 1.2352095833423058)) (0.14000000000000012 (1.4753046508807501 0.09472176659392792) (-1.3488506990538995 1.3163376545076142)) (0.15000000000000016 (1.4613699975718402 0.10826965834808744) (-1.4376907019867997 1.3923036558232782)) (0.16000000000000017 (1.4465592741128204 0.12254752574679212) (-1.5239992538321638 1.462174796311749)) (0.17 (1.4308997666190004 0.137489661843808) (-1.6073817140120092 1.524998544012935)) (0.18000000000000002 (1.4144226961605564 0.15302085970306042) (-1.6874478478233603 1.5798325978587788)) (0.19000000000000003 (1.397163111223637 0.1690566702841109) (-1.7638244849232263 1.6257759764340833)) 4982 more elided]

Peek at the final state:

(peek raw-regular-data)
(49.99999999999343 (1.0874071859825067 0.5151887602030968) (1.946135177358664 -0.4205806100888969))

Measurements, Data Transformation

Next we'll chart the measurements trapped in those sequences of state tuples.

The exercise asks us to graph the energy of the system as a function of time. Composing Lagrangian->energy with L-double-pendulum gives us a new function (of a state tuple!) that will return the current energy in the system.:

(def L-energy
(compose
Lagrangian->energy
L-double-pendulum))
#object[clojure.lang.AFunction$1 0xd7222f9 "
clojure.lang.AFunction$1@d7222f9"
]

energy-monitor returns a function of state that stores an initial energy value in its closure, and returns the delta of each new state's energy as compared to the original.

(defn energy-monitor [energy-fn initial-state]
(let [initial-energy (energy-fn initial-state)]
(fn [state]
(- (energy-fn state) initial-energy))))
#object[sicmutils$energy_monitor 0x5f343b99 "
sicmutils$energy_monitor@5f343b99"
]

Finally, the large transform-data function. The charting library we'll use likes Clojure dictionaries; transform-data turns our raw data into a sequence of dictionary with all values we might care to explore. This includes:

  • the generalized coordinate angles
  • the generalized velocities of those angles
  • the rectangular coordinates of each pendulum bob
  • :d-energy, the error in energy at each point in the simulation

Here is transform-data:

(defn transform-data [xs]
(let [energy-fn (L-energy m1 m2 l1 l2 g)
monitor (energy-monitor energy-fn (first xs))
xform (angles->rect l1 l2)
pv (principal-value Math/PI)]
(map (fn [[t [theta1 theta2] [thetadot1 thetadot2] :as state]]
(let [[x1 y1 x2 y2] (xform state)]
{:t t
:theta1 (pv theta1)
:x1 x1
:y1 y1
:theta2 (pv theta2)
:x2 x2
:y2 y2
:thetadot1 thetadot1
:thetadot2 thetadot2
:d-energy (monitor state)}))
xs)))
#object[sicmutils$transform_data 0x57ff28ca "
sicmutils$transform_data@57ff28ca"
]

The following forms transform the raw data for each initial condition and bind the results to chaotic-data and regular-data for exploration.

(def chaotic-data
(doall
(transform-data raw-chaotic-data)))
({:d-energy 0 :t 0 :theta1 1.5707963267948966 :theta2 -3.141592653589793 :thetadot1 0 :thetadot2 0 :x1 1 :x2 0.09999999999999998 :y1 -6.123233995736766e-17 :y2 1.0409497792752503e-16} {:d-energy 1.0140715910008495e-14 :t 0.009999999999999998 :theta1 1.5703063268694102 :theta2 -3.1411026535597264 :thetadot1 -0.09799995529340401 :thetadot2 0.09800001803950574 :x1 0.9999998799500389 :x2 0.0999998799500389 :y1 -0.0004899999058782767 :y2 -0.0004900000000004213} {:d-energy 1.4970186067127926e-14 :t 0.019999999999999993 :theta1 1.5688363315635692 :theta2 -3.13963265166559 :thetadot1 -0.19599856940634308 :thetadot2 0.19600057725813616 :x1 0.9999980792099615 :x2 0.09999807920996151 :y1 -0.0019599939764141892 :y2 -0.001960000000002052} {:d-energy 5.588801509045352e-15 :t 0.03 :theta1 1.5663863811113454 :theta2 -3.137182631672509 :thetadot1 -0.2939891369903126 :thetadot2 0.2940043833599193 :x1 0.9999902762052928 :x2 0.09999027620529533 :y1 -0.00440993138973976 :y2 -0.004410000000099388} {:d-energy 8.703536543884369e-16 :t 0.04000000000000001 :theta1 1.562956631954858 :theta2 -3.1337525304529295 :thetadot1 -0.3919542297291326 :thetadot2 0.39201846924143363 :x1 0.9999692697498002 :x2 0.09996926974988263 :y1 -0.00783961453461278 :y2 -0.007840000001755618} {:d-energy 2.7515706245392195e-14 :t 0.05000000000000001 :theta1 1.558547490683088 :theta2 -3.1293421839307456 :thetadot1 -0.48986036114525866 :thetadot2 0.49005634949325527 :x1 0.9999249839448732 :x2 0.09992498394607396 :y1 -0.012248529823821765 :y2 -0.01225000001633653} {:d-energy 4.061633793596904e-14 :t 0.06000000000000002 :theta1 1.5531598010356917 :theta2 -3.1239512515796313 :thetadot1 -0.5876527189011422 :thetadot2 0.5881401515558404 :x1 0.9998444805107916 :x2 0.09984448052149153 :y1 -0.017635611475232873 :y2 -0.01764000010109381} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} 4981 more elided)
(def regular-data
(doall
(transform-data raw-regular-data)))
({:d-energy 0 :t 0 :theta1 1.5707963267948966 :theta2 0 :thetadot1 0 :thetadot2 0 :x1 1 :x2 1.9 :y1 -6.123233995736766e-17 :y2 -1.1634144591899857e-16} {:d-energy 5.802592669571072e-14 :t 0.010000000000000004 :theta1 1.5703063268694089 :theta2 0.0004899998209110522 :thetadot1 -0.09799995529342519 :thetadot2 0.09799989254735403 :x1 0.9999998799500389 :x2 1.8999998799500388 :y1 -0.0004899999058796089 :y2 -0.0004899999999985766} {:d-energy 5.864348825315846e-14 :t 0.020000000000000004 :theta1 1.5688363315635574 :theta2 0.001959988538482646 :thetadot1 -0.19599856941148464 :thetadot2 0.19599656156730913 :x1 0.9999980792099614 :x2 1.8999980792099613 :y1 -0.0019599939764259576 :y2 -0.001959999999996855} {:d-energy 6.352846956150915e-14 :t 0.03 :theta1 1.5663863811107515 :theta2 0.0044098694512942896 :thetadot1 -0.29398913718793995 :thetadot2 0.29397389111112554 :x1 0.9999902762052901 :x2 1.8999902762052876 :y1 -0.004409931390333724 :y2 -0.004409999999899608} {:d-energy -3.061844292670412e-14 :t 0.040000000000000015 :theta1 1.5629566319443307 :theta2 0.007839266569336433 :thetadot1 -0.39195423235948346 :thetadot2 0.39188999674445024 :x1 0.9999692697497177 :x2 1.899969269749635 :y1 -0.007839614545139812 :y2 -0.0078399999982463} {:d-energy -1.9903927576234037e-13 :t 0.050000000000000024 :theta1 1.5585474905851686 :theta2 0.012247202807576447 :thetadot1 -0.4898603807158662 :thetadot2 0.4896644213695836 :x1 0.9999249839436738 :x2 1.8999249839424732 :y1 -0.012248529921733869 :y2 -0.012249999983670306} {:d-energy -2.2646178447058174e-13 :t 0.06000000000000002 :theta1 1.553159800430448 :theta2 0.01763165101050902 :thetadot1 -0.587652819630181 :thetadot2 0.5871655362912933 :x1 0.9998444805001178 :x2 1.899844480489422 :y1 -0.01763561208038238 :y2 -0.01763999989892792} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} {10 more elided} 4982 more elided)

Here's the final, transformed chaotic state:

(last chaotic-data)
{:d-energy 6.194265538814605e-11 :t 49.99999999999393 :theta1 -0.7253216905180996 :theta2 2.07191889133761 :thetadot1 2.630986320591619 :thetadot2 -5.649480294737957 :x1 -0.6633761935138307 :x2 0.2140990335577101 :y1 -0.7482860588565716 :y2 -0.9483791019034198}

And the similar regular state:

(last regular-data)
{:d-energy -1.2799959582955405e-11 :t 49.99999999999343 :theta1 1.0874071859825067 :theta2 0.5151887602030968 :thetadot1 1.946135177358664 :thetadot2 -0.4205806100888969 :x1 0.8854247969960878 :x2 1.7849697882336697 :y1 -0.46478266842088334 :y2 -0.43616783416697863}

Data Visualization Utilities

Vega-Lite allows us to visualizing the system.

I am not a pro here, but this does the trick for now.

First, a function to transform the dictionaries we generated above into a sequence of x, y coordinates tagged with distinct IDs for each pendulum bob's points:

(defn points-data [data]
(mapcat (fn [{:keys [t x1 y1 x2 y2]}]
[{:t t
:x x1
:y y1
:id :p1}
{:t t
:x x2
:y y2
:id :p2}])
data))
#object[sicmutils$points_data 0x227c1be2 "
sicmutils$points_data@227c1be2"
]

segments-data generates endpoints of the pendulum segments, bob-to-bob:

(defn segments-data [data]
(mapcat (fn [{:keys [t x1 y1 x2 y2]}]
[{:t t
:x 0
:y 0
:x2 x1
:y2 y1
:id :p1}
{:t t
:x x1
:y y1
:x2 x2
:y2 y2
:id :p2}])
data))
#object[sicmutils$segments_data 0x3e0580be "
sicmutils$segments_data@3e0580be"
]

Visualizations

a helper function that should be in clojure.core

(defn deep-merge [v & vs]
(letfn [(rec-merge [v1 v2]
(if (and (map? v1) (map? v2))
(merge-with deep-merge v1 v2)
v2))]
(when (some identity vs)
(reduce #(rec-merge %1 %2) v vs))))
#object[sicmutils$deep_merge 0x40938c14 "
sicmutils$deep_merge@40938c14"
]

With those tools in hand, let's make some charts. I'll call this first chart the system-inspector; this function will return a chart that will let us evolve the system with a time slider and monitor both angles, the energy error, and the pendulum bob itself as it evolves through time.

(defn system-inspector [data]
{:config {:bar {:binSpacing 1, :discreteBandSize 5, :continuousBandSize 5}},
:datasets {:points data}
:vconcat [{:hconcat [{:layer (mapv (partial deep-merge
{:encoding {:y {:field "y", :type "quantitative"},
:color {:field :id, :type :nominal},
:opacity {:condition {:test "abs(selected_t - datum['t']) < 0.00001", :value 1},
:value 0.3},
:x {:field "x", :type "quantitative"},
:y2 {:field "y2", :type "quantitative"},
:x2 {:field "x2", :type "quantitative"},
:tooltip [{:field "x", :type "quantitative"}
{:field "y", :type "quantitative"}]},
:height 300,
:width 400})
[{:data {:values (points-data data)}
:encoding {:size {:condition
{:test "abs(selected_t - datum['t']) < 0.00001", :value 200},
:value 5}
:opacity {:value 0.3}}
:selection {:selected {:fields [:t],
:type :single,
:bind {:t {:min 0.01, :max 49.99, :input :range, :step 0.01}}}}
:mark {:type "circle"}}
{:data {:values (segments-data data)}
:encoding {:opacity {:value 0}}
:mark "rule"}])}
{:encoding {:y {:field :d-energy, :type "quantitative"},
:size {:condition
{:test "abs(selected_t - datum['t']) < 0.00001", :value 200},
:value 5},
:x {:field :t, :type "quantitative"},
:y2 {:field "y2", :type "quantitative"},
:x2 {:field "x2", :type "quantitative"},
:tooltip [{:field :t, :type "quantitative"}
{:field :d-energy, :type "quantitative"}]},
:mark {:type "circle"},
:width 400,
:height 300,
:data {:name :points}}]}
{:hconcat (mapv (partial deep-merge {:encoding {:y {:field :unassigned,
:type "quantitative",
:scale {:domain [(- Math/PI) Math/PI]}},
:size {:condition {:test "abs(selected_t - datum['t']) < 0.00001", :value 200},
:value 5},
:x {:field :t, :type "quantitative"},
:y2 {:field "y2", :type "quantitative"},
:x2 {:field "x2", :type "quantitative"},
:tooltip [{:field :t, :type "quantitative"}
{:field :unassigned, :type "quantitative"}]},
:mark {:type "circle"},
:width 400,
:height 300,
:data {:name :points}})
[{:encoding {:y {:field :theta1},
:tooltip [{:field :theta1}]}}
{:encoding {:y {:field :theta2},
:tooltip [{:field :theta2}]}}])}]})
#object[sicmutils$system_inspector 0x6e0455b4 "
sicmutils$system_inspector@6e0455b4"
]

Here's a system monitor for the chaotic initial condition:

(clerk/vl
(system-inspector chaotic-data))
Loading...

And again for the regular initial condition:

(clerk/vl
(system-inspector regular-data))
Loading...

Generalized coordinates, velocities

angles-plot generates a plot, with no animation, showing both theta angles and their associated velocities:

(defn angles-plot [data]
(let [quarter-spec {:encoding {:y {:field :unassigned
:type "quantitative"}
:size {:value 5}
:x {:field :t :type "quantitative"}
:y2 {:field "y2" :type "quantitative"}
:x2 {:field "x2" :type "quantitative"}
:tooltip [{:field :t :type "quantitative"}
{:field :unassigned :type "quantitative"}]}
:mark {:type "circle"}
:width 400
:height 300
:data {:name :points}}]
{:config {:bar {:binSpacing 1 :discreteBandSize 5 :continuousBandSize 5}}
:datasets {:points data}
:width 800
:height 600
:hconcat [{:vconcat (mapv (partial deep-merge quarter-spec)
[{:encoding {:y {:field :theta1
:scale {:domain [(- Math/PI) Math/PI]}}
:tooltip [{:field :theta1}]}}
{:encoding {:y {:field :theta2
:scale {:domain [(- Math/PI) Math/PI]}}
:tooltip [{:field :theta2}]}}])}
{:vconcat (mapv (partial deep-merge quarter-spec)
[{:encoding {:y {:field :thetadot1}
:tooltip [{:field :theta1}]}}
{:encoding {:y {:field :thetadot2}
:tooltip [{:field :theta2}]}}])}]}))
#object[sicmutils$angles_plot 0x2ce5e09d "
sicmutils$angles_plot@2ce5e09d"
]

Here are the angles for the chaotic initial condition:

(clerk/vl
(angles-plot chaotic-data))
Loading...

And the regular initial condition:

(clerk/vl
(angles-plot regular-data))
Loading...

Double Double Pendulum!

No visualizations here yet, but the code works well.

(defn L-double-double-pendulum [m1 m2 l1 l2 g]
(fn [[t [thetas1 thetas2] [qdots1 qdots2]]]
(let [s1 (up t thetas1 qdots1)
s2 (up t thetas2 qdots2)]
(+ ((L-double-pendulum m1 m2 l1 l2 g) s1)
((L-double-pendulum m1 m2 l1 l2 g) s2)))))
#object[sicmutils$L_double_double_pendulum 0x43dadd07 "
sicmutils$L_double_double_pendulum@43dadd07"
]
(def dd-state-derivative
(compose
Lagrangian->state-derivative
L-double-double-pendulum))
#object[clojure.lang.AFunction$1 0x3e7785cf "
clojure.lang.AFunction$1@3e7785cf"
]
(defn safe-log [x]
(if (< x 1e-60)
-138.0
(Math/log x)))
#object[sicmutils$safe_log 0x62d85d39 "
sicmutils$safe_log@62d85d39"
]
(defn divergence-monitor []
(let [pv (principal-value Math/PI)]
(fn [[_ [thetas1 thetas2]]]
(safe-log
(Math/abs
(pv
(- (nth thetas1 1)
(nth thetas2 1))))))))
#object[sicmutils$divergence_monitor 0x2ebd0d23 "
sicmutils$divergence_monitor@2ebd0d23"
]
(defn run-double-double!
"Two different initializations, slightly kicked"
[step horizon initial-q1]
(let [initial-q2 (+ initial-q1 (up 0.0 1e-10))
initial-qdot (up 0.0 0.0)
initial-state (up 0.0
(up initial-q1 initial-q2)
(up initial-qdot initial-qdot))
collector (atom (transient []))]
((evolve dd-state-derivative m1 m2 l1 l2 g)
initial-state
step
horizon
{:compile? true
:epsilon 1.0e-13 ; = (max-norm 1.e-13)
:observe (fn [_ state]
(swap! collector conj! state))})
(persistent! @collector)))
#object[sicmutils$run_double_double_BANG_ 0x2ddff161 "
sicmutils$run_double_double_BANG_@2ddff161"
]
(def raw-dd-chaotic-data
(run-double-double! step horizon chaotic-initial-q))
[(0 ((1.5707963267948966 3.141592653589793) (1.5707963267948966 3.141592653689793)) ((0 0) (0 0))) (0.010000000000000028 ((1.5703063268694106 3.1420826536198607) (1.5703063268694106 3.1420826537198603)) ((-0.09799995529340819 0.09800001803950396) (-0.09799995529339359 0.09800001803951128))) (0.020000000000000028 ((1.5688363315635696 3.1435526555139974) (1.56883633156357 3.143552655613997)) ((-0.19599856940634316 0.19600057725813586) (-0.19599856940622787 0.19600057725819175))) (0.030000000000000023 ((1.5663863811113463 3.1460026755070785) (1.5663863811113492 3.1460026756070794)) ((-0.2939891369903127 0.294004383359919) (-0.2939891369899236 0.2940043833601067))) (0.04000000000000003 ((1.5629566319548585 3.149432776726658) (1.5629566319548678 3.1494327768266617)) ((-0.39195422972913285 0.3920184692414333) (-0.3919542297282112 0.3920184692418772))) (0.05000000000000002 ((1.5585474906830885 3.1538431232488433) (1.558547490683111 3.1538431233488535)) ((-0.4898603611452543 0.4900563494932569) (-0.4898603611434557 0.4900563494941234))) (0.05999999999999998 ((1.5531598010357188 3.1592340555999696) (1.5531598010357643 3.1592340556999923)) ((-0.5876527189006441 0.5881401515560127) (-0.5876527188975403 0.5881401515575083))) (0.06999999999999997 ((1.546795082923498 3.1656061873414485) (1.5467950829235817 3.165606187441491)) ((-0.6852500466004207 0.6863026951826584) (-0.6852500465955016 0.6863026951850292))) (0.07999999999999997 ((1.5394558213585319 3.1729605220096335) (1.5394558213586762 3.172960522109706)) ((-0.7825398067912416 0.7845894747326814) (-0.7825398067839211 0.7845894747362129))) (0.08999999999999996 ((1.5311458015778847 3.1812985891270515) (1.5311458015781167 3.1812985892271666)) ((-0.8793738164107314 0.883060478384545) (-0.8793738164003545 0.8830604783895578))) (0.09999999999999995 ((1.5218704844072386 3.1906225972318785) (1.5218704844075932 3.190622597332052)) ((-0.9755646118932003 0.9817917559202847) (-0.9755646118790545 0.9817917559271311))) (0.10999999999999996 ((1.511637413000684 3.2009356008752703) (1.511637413001202 3.2009356009755234)) ((-1.0708828683338691 1.0808766242569892) (-1.0708828683151996 1.0808766242660481))) (0.11999999999999993 ((1.5004566385363853 3.2122416773262406) (2 more elided)) (2 more elided)) (3 more elided) (3 more elided) (3 more elided) (3 more elided) (3 more elided) (3 more elided) (3 more elided) 4982 more elided]

Looks good:

(peek raw-dd-chaotic-data)
(50.0000000000049 ((-1.7906385582912365 34.21061163038391) (0.11078072559939628 22.44932156460319)) ((1.6211021444359892 -1.7160256002080234) (-2.7931350134821753 -2.926881404141952)))

Next, the regular initial condition:

(def raw-dd-regular-data
(run-double-double! step horizon regular-initial-q))
[(0 ((1.5707963267948966 0) (1.5707963267948966 1e-10)) ((0 0) (0 0))) (0.009999999999999988 ((1.5703063268694084 0.000489999820911035) (1.5703063268694089 0.0004899999209109449)) ((-0.09799995529342523 0.09799989254735417) (-0.0979999552934109 0.09799989254731847))) (0.019999999999999987 ((1.568836331563557 0.0019599885384826306) (1.568836331563558 0.001959988638481201)) ((-0.19599856941148472 0.19599656156730927) (-0.19599856941136964 0.1959965615670233))) (0.02999999999999999 ((1.5663863811107508 0.004409869451294277) (1.5663863811107541 0.004409869551287038)) ((-0.29398913718793995 0.2939738911111257) (-0.29398913718755126 0.2939738911101606))) (0.039999999999999994 ((1.5629566319443307 0.007839266569336418) (1.5629566319443413 0.007839266669313547)) ((-0.39195423235948357 0.3918899967444506) (-0.3919542323585623 0.39188999674216385))) (0.05000000000000005 ((1.5585474905851702 0.012247202807576957) (1.5585474905851944 0.012247202907521133)) ((-0.48986038071586974 0.48966442136959265) (-0.48986038071407273 0.48966442136513033))) (0.060000000000000074 ((1.5531598004304505 0.01763165101050918) (1.5531598004304983 0.017631651110393512)) ((-0.587652819630179 0.5871655362912892) (-0.587652819627078 0.5871655362835896))) (0.07000000000000008 ((1.5467950801044654 0.023988960989759927) (1.5467950801045527 0.02398896108954591)) ((-0.6852504482852049 0.6841983954406183) (-0.685250448280292 0.6841983954284209))) (0.08000000000000007 ((1.5394558106911906 0.03131316894470855) (1.5394558106913383 0.031313169044344114)) ((-0.7825411344032182 0.7804934369649749) (-0.7825411343959122 0.7804934369468343))) (0.09000000000000007 ((1.5311457671636506 0.03959520040392475) (1.5311457671638862 0.03959520050334255)) ((-0.8793776135754036 0.8756965939174551) (-0.8793776135650585 0.875696593891763))) (0.10000000000000014 ((1.5218703866044465 0.048821984302405225) (1.521870386604805 0.04882198440152112)) ((-0.9755742884740192 0.9693615460955163) (-0.9755742884599374 0.969361546060533))) (0.11000000000000025 ((1.5116371624239835 0.05897550388894217) (1.5116371624245044 0.058975503987654145)) ((-1.0709052950930689 1.0609449896377214) (-1.0709052950745195 1.0609449895916154))) (0.12000000000000025 ((1.5004560499204538 0.07003181939656317) (2 more elided)) (2 more elided)) (3 more elided) (3 more elided) (3 more elided) (3 more elided) (3 more elided) (3 more elided) (3 more elided) 4981 more elided]

Peek at the final state:

(peek raw-dd-regular-data)
(49.999999999997435 ((1.0874071859817587 0.5151887602030217) (1.0874071853115117 0.5151887602834131)) ((1.9461351773601867 -0.42058061008572345) (1.9461351776231073 -0.4205806056566918)))