Skip to content

Instantly share code, notes, and snippets.

@travisdoesmath
Last active April 19, 2020 05:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save travisdoesmath/32eea7d1cb47c18bfabcab65abc9b36c to your computer and use it in GitHub Desktop.
Save travisdoesmath/32eea7d1cb47c18bfabcab65abc9b36c to your computer and use it in GitHub Desktop.
Double Pendulums are Still Chaotic

Updated from this block to be easier to modify. Showing the sensitive dependence to initial conditions, 60 double pendulums with masses from 1 to 1.01. Click to restart with a new starting angle.

in app.js, nPendulums is the number of pendulums to creaste, pendulums is an array of pendulums, using the Pendulum class. Options you can send are:

  • m1, the mass of the first bob
  • m2, the mass of the second bob
  • l1, the length of the upper shaft
  • l2, the length of the lower shaft
  • theta1, the starting angle of the first shaft
  • theta2, the starting angle of the second shaft
  • p1, the starting momentum of the first bob
  • p2, the starting momentum of the second bob

Inspired by this triple pendulum GIF

var nPendulums = 60;
var pendulums = d3.range(nPendulums).map(x => new Pendulum({m2: 1 + 0.01*x/nPendulums, theta1:0.75*Math.PI}))
var fadeBackground = true;
var svg = d3.select("svg")
width = +svg.attr("width"),
height = +svg.attr("height"),
g = svg.append("g").attr("transform", "translate(" + width*.5 + "," + height*.5 + ")");
color = d3.scaleSequential(d3.interpolateRainbow).domain([0, nPendulums]);
svg.on('click', e => {
var mousePos = d3.mouse(svg.node());
reset(mousePos);
});
var canvas = d3.select("canvas");
var context = canvas.node().getContext('2d');
var scale = d3.scaleLinear().domain([0,1]).range([0,100])
var path = d3.line()
.x(function(d) { return scale(d.l1*Math.sin(d.theta1)+d.l2*Math.sin(d.theta2)); })
.y(function(d) { return scale(d.l1*Math.cos(d.theta1)+d.l2*Math.cos(d.theta2)); })
var update = function() {
var oldCoords = pendulums.map(p => p.getCoords());
pendulums.forEach(p => p.evolve());
var coords = pendulums.map(p => p.getCoords());
draw(oldCoords, coords);
}
var trailOpacity = 1;
var maxThetaDelta = 0;
var opacityScale = d3.scaleLinear().domain([0, 2*Math.PI]).range([1, 0])
var draw = function(oldCoords, coords) {
if (maxThetaDelta < 2*Math.PI) {
if (fadeBackground) {
maxThetaDelta = Math.max(maxThetaDelta, Math.abs(d3.max(pendulums, d => d.theta1) - d3.min(pendulums, d => d.theta1)))
//trailOpacity -= maxThetaDelta / 1500;
trailOpacity = opacityScale(maxThetaDelta)
//trailOpacity = opacityScale(Math.abs(pendulums[nPendulums - 1].theta1 - pendulums[0].theta1))
}
canvas.style('opacity', trailOpacity);
}
for (var i = coords.length - 1; i >= 0; i--) {
context.beginPath();
context.strokeStyle = color(i);
context.lineWidth = 2;
context.moveTo(scale(oldCoords[i].x2) + width/2, scale(oldCoords[i].y2) + height/2);
context.lineTo(scale(coords[i].x2) + width/2, scale(coords[i].y2) + height/2);
context.stroke();
}
var pendulum = g.selectAll(".pendulum").data(coords, function(d, i) { return i; })
var pendulumEnter = pendulum.enter()
.append("g").attr("class","pendulum")
pendulumEnter.append("line").attr("class", "firstShaft shaft")
pendulumEnter.append("line").attr("class", "secondShaft shaft")
pendulumEnter.append("circle").attr("class", "firstBob bob").attr("r",3)
pendulumEnter.append("circle").attr("class", "secondBob bob").attr("r",7)
var shaft1 = pendulum.select(".firstShaft")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", d => scale(d.x1))
.attr("y2", d => scale(d.y1))
.attr('stroke', (d, i) => color(i))
var shaft2 = pendulum.select(".secondShaft")
.attr("x1", d => scale(d.x1))
.attr("y1", d => scale(d.y1))
.attr("x2", d => scale(d.x2))
.attr("y2", d => scale(d.y2))
.attr('stroke', (d, i) => color(i))
var bob1 = pendulum.select(".firstBob")
.attr("cx", d => scale(d.x1))
.attr("cy", d => scale(d.y1))
.attr('fill', (d, i) => color(i))
.attr('opacity', 1)
var bob2 = pendulum.select(".secondBob")
.attr("cx", d => scale(d.x2))
.attr("cy", d => scale(d.y2))
.attr('fill', (d, i) => color(i))
.attr('stroke', (d, i) => d3.color(color(i)).darker())
.attr('stroke-width', 2)
}
var reset = function(mousePos) {
console.log(mousePos)
var theta1 = 0.5*Math.PI + Math.atan2(height/2 - mousePos[1], mousePos[0] - width/2)
trailOpacity = 1;
maxThetaDelta = 0;
pendulums = d3.range(nPendulums).map(x => new Pendulum({m2: 1 + 0.01*x/nPendulums, theta1:theta1}));
context.clearRect(0, 0, width, height);
}
var run = setInterval(() => { update() }, 2);
class Pendulum {
constructor(opts) {
// default values
this.l1=1;
this.l2=1;
this.m1=1;
this.m2=1;
this.G=9.8
this.theta1=0.49*Math.PI;
this.theta2=1.0*Math.PI;
this.p1=0;
this.p2=0;
['l1','l2','m1','m2','G','theta1','theta2','p1','p2'].map(k => opts[k] ? this[k] = opts[k] : null)
}
theta1dot(theta1, theta2, p1, p2) {
return (p1*this.l2 - p2*this.l1*Math.cos(theta1 - theta2))/(this.l1**2*this.l2*(this.m1 + this.m2*Math.sin(theta1 - theta2)**2));
}
theta2dot(theta1, theta2, p1, p2) {
return (p2*(this.m1+this.m2)*this.l1 - p1*this.m2*this.l2*Math.cos(theta1 - theta2))/(this.m2*this.l1*this.l2**2*(this.m1 + this.m2*Math.sin(theta1 - theta2)**2));
}
p1dot(theta1, theta2, p1, p2) {
var A1 = (p1*p2*Math.sin(theta1 - theta2))/(this.l1*this.l2*(this.m1 + this.m2*Math.sin(theta1 - theta2)**2)),
A2 = (p1**2*this.m2*this.l2**2 - 2*p1*p2*this.m2*this.l1*this.l2*Math.cos(theta1 - theta2) + p2**2*(this.m1 + this.m2)*this.l1**2)*Math.sin(2*(theta1 - theta2))/(2*this.l1**2*this.l2**2*(this.m1 + this.m2*Math.sin(theta1 - theta2)**2)**2);
return -(this.m1 + this.m2)*this.G*this.l1*Math.sin(theta1) - A1 + A2;
}
p2dot(theta1, theta2, p1, p2) {
var A1 = (p1*p2*Math.sin(theta1 - theta2))/(this.l1*this.l2*(this.m1 + this.m2*Math.sin(theta1 - theta2)**2)),
A2 = (p1**2*this.m2*this.l2**2 - 2*p1*p2*this.m2*this.l1*this.l2*Math.cos(theta1 - theta2) + p2**2*(this.m1 + this.m2)*this.l1**2)*Math.sin(2*(theta1 - theta2))/(2*this.l1**2*this.l2**2*(this.m1 + this.m2*Math.sin(theta1 - theta2)**2)**2);
return -this.m2*this.G*this.l2*Math.sin(theta2) + A1 - A2;
}
f(Z) {
return [this.theta1dot(Z[0], Z[1], Z[2], Z[3]), this.theta2dot(Z[0], Z[1], Z[2], Z[3]), this.p1dot(Z[0], Z[1], Z[2], Z[3]), this.p2dot(Z[0], Z[1], Z[2], Z[3])];
}
RK4(tau) {
var Y1 = this.f([this.theta1, this.theta2, this.p1, this.p2]).map(d => d*tau);
var Y2 = this.f([this.theta1 + 0.5*Y1[0], this.theta2 + 0.5*Y1[1], this.p1 + 0.5*Y1[2], this.p2 + 0.5*Y1[3]]).map(d => d*tau);
var Y3 = this.f([this.theta1 + 0.5*Y2[0], this.theta2 + 0.5*Y2[1], this.p1 + 0.5*Y2[2], this.p2 + 0.5*Y2[3]]).map(d => d*tau);
var Y4 = this.f([this.theta1 + Y3[0], this.theta2 + Y3[1], this.p1 + Y3[2], this.p2 + Y3[3]]).map(d => d*tau);
return [
this.theta1 + Y1[0]/6 + Y2[0]/3 + Y3[0]/3 + Y4[0]/6,
this.theta2 + Y1[1]/6 + Y2[1]/3 + Y3[1]/3 + Y4[1]/6,
this.p1 + Y1[2]/6 + Y2[2]/3 + Y3[2]/3 + Y4[2]/6,
this.p2 + Y1[3]/6 + Y2[3]/3 + Y3[3]/3 + Y4[3]/6,
]
}
evolve(t=0.005) {
var nextState = this.RK4(t);
this.theta1 = nextState[0];
this.theta2 = nextState[1];
this.p1 = nextState[2];
this.p2 = nextState[3];
return this.getCoords();
}
getCoords() {
return {
'x1':this.l1*Math.sin(this.theta1),
'y1':this.l1*Math.cos(this.theta1),
'x2':this.l1*Math.sin(this.theta1) + this.l2*Math.sin(this.theta2),
'y2':this.l1*Math.cos(this.theta1) + this.l2*Math.cos(this.theta2)
}
}
}
<html>
<head>
<style>
.shaft {
stroke-width: 2px;
}
svg {
position:absolute;
top:0px;
left:0px;
}
canvas {
position:absolute;
top:0px;
left:0px;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.7.4/d3.js"></script>
<script src="./double_pendulum.js"></script>
</head>
<body>
<canvas width="960" height="500"></canvas>
<svg width="960" height="500"></svg>
</body>
<script src="app.js"></script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment