 October 03 2016

# How to Keep your Train from going off the Rails

Thirty years ago, I received a beautiful electric train set.It came with three different kinds of rails: Thirty years ago, I received a beautiful electric train set.

It came with three different kinds of rails: 1. straight rails,
2. curved rails,
3. and… curved rails… what ? Again?!

Years later, this third type of rail still puzzled me. The second kind of rail was definitely arc-shaped, but the third had a funny curve to it.

One thing was sure: when I would build an oval run, if I didn’t put the right rails in the right place, my train was guaranteed to derail at high speed. ## All aboard the clothoid!

Enough with the teaser. The mystery curve was called a clothoid, or Euler spiral, or just spiral among friends.

A clothoid is the result of a specific mathematical formula that defines a curve whose curvature changes linearly with its curve length.

By definition, the radius of a curve is:

• the radius of a circle (in our case, the second type of rail),
• infinity for straight rails (i.e. the same rails with an infinite radius).

Following the sequence below (called “S-C-S” for Spiral-Curve-Spiral):

• rail-straight, R = +∞,
• rail-spiral, R : +∞ → 60 cm,
• rail-curve, R = 60 cm,
• rail-spiral, R : 60 cm → +∞,
• rail-straight, R = +∞,

the train trip becomes much smoother, without the risk of derailments!

To better understand the effect, let’s take another example from railroads: The blue curve is exclusively made up of arcs from a circle with a 60-cm radius, while the green and red curve is made up of two different spirals.

If we now represent the curve (1/R) along the road, this is what we get: The second itinerary is longer, but free of curvature breaks. This type of curve has many applications in civil engineering: for roads, bridges, railroads, mechanics.

Remember this next time you take a highway exit safely at 100 km/h!

## Why are clothoids also called spirals?

Looking at the math behind the curve, if you draw such a curve on an infinite trajectory, this is what you get:

Two symmetrical spirals.

 R Radius of curvature at any given point in a trajectory. Given that R = +∞ at the beginning of the trajectory. φ Angle between the tangent to the trajectory at any given point and the axis of the abscissa. φ = 0 at the beginning. L Distance travelled along the curve at any given point. L = 0 at the beginning. A Constant describing the change in the radius of the curve along the trajectory, by definition: 1/R = L x 1/A2, i.e. A2 = RL Note that in this figure, the two φ angles are identical:

• φ = internal angle between (d1) and (d2).
• (d3) is perpendicular to (d1) by construction.
• (d4) is perpendicular to (d2), since it is the tangent of circle CM at point M.
• Therefore the angle between (d3) and (d4) is also φ.

## Let’s draw a clothoid!

Now let’s draw our own spiral.

If we imagine the trajectory of point M(x,y) along the following itinerary:

• from the curved abscissa L1 (=0),
• to the curved abscissa L2 (=L length of trajectory),

we get: Note that φ is indeed a function of l: So, given: we get: which gives us the following JavaScript code:

function drawSpiral(A, L1, L2)
{
var a  = A*Math.sqrt(2);
var S1 = L1/a;
var S2 = L2/a;

var s=S1;
var ds = Math.abs(S2-S1)/10000;

var M = new Point(0,0);

while (s
   Résultat .wrapper_result { position:relative; } .canvas_img { position: absolute; float:left; background-color: #e8e8e8; border: 2px solid #777; border-radius: 10px; padding:10px ; margin:10px; box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19); } @media all and (max-width: 940px) { .canvas_img { position: initial; float:none; } .wrapper_canvas canvas { width:100%; height:auto; } } Lenght : StartRadius : isCCW: TrueFalse isEntry: TrueFalse ClothoideConstant: /* DRAWING PRIMITIVES */ /* https://github.com/chtimi59/sm2d.js */ var canvas = document.getElementById("myCanvas"); var ctx = canvas.getContext("2d"); function f2str(f,digit) { if (digit===undefined) digit = 1; var p = Math.pow(10,digit); return Math.round(f*p)/p; } function clear() { ctx.clearRect(0, 0, canvas.width, canvas.height); xaxis(0); yaxis(0); xmark(1); ymark(1); } var MINX = -10; var MAXX = +10; var MINY = -10; var WIDTH = (MAXX-MINX); var HEIGHT = WIDTH; var MAXY = MINY+WIDTH; var SCALE = canvas.width/WIDTH; function coord(pt) { return new Point( SCALE*(pt.x - MINX), canvas.height - SCALE*(pt.y - MINY)); } function Point(vx, vy) { this.x = vx; this.y = vy; this.add = function(p) { return new Point(this.x+p.x, this.y+p.y); } this.norm = function() { return Math.sqrt(this.x*this.x + this.y*this.y); } } function Vector(vstart, vangle, vlenght) { this.start = new Point(vstart.x,vstart.y); this.angle = vangle; this.lenght = vlenght; this.end = function() { return new Point(this.start.x + this.lenght*Math.cos(this.angle), this.start.y + this.lenght*Math.sin(this.angle)) } this.middle = function() { var e = this.end(); return new Point(0.5*this.start.x+0.5*e.x, 0.5*this.start.y+0.5*e.y); } } function Vector2(p1,p2) { var diff = new Point(p2.x-p1.x, p2.y-p1.y); var angle = Math.atan2(diff.y,diff.x); var lenght = diff.norm(); return new Vector(p1, angle, lenght); } function lineTo(pt) { var pt2 = coord(pt); ctx.lineTo(pt2.x,pt2.y); } function moveTo(pt) { var pt2 = coord(pt); ctx.moveTo(pt2.x,pt2.y); } function line(a,b) { moveTo(a); lineTo(b); } function yaxis(y,w,l) { if (!w) w=1; if(!l) l=WIDTH; ctx.beginPath(); line(new Point(-l,y),new Point(+l,y)); ctx.lineWidth = w; ctx.strokeStyle = '#555'; ctx.stroke(); } function xaxis(x,w,l) { if (!w) w=1; if(!l) l=HEIGHT; ctx.beginPath(); line(new Point(x,-l),new Point(x,+l)); ctx.lineWidth = w; ctx.strokeStyle = '#555'; ctx.stroke(); } function xmark(s) { for(x=0;x<MAXX;x+=s) xaxis(x,1,HEIGHT/100); for(x=0;x>MINX;x-=s) xaxis(x,1,HEIGHT/100); } function ymark(s) { for(y=0;y<MAXY;y+=s) yaxis(y,1,WIDTH/100); for(y=0;y>MINY;y-=s) yaxis(y,1,WIDTH/100); } function ray(v,name,color) { var p1 = v.start; var p2 = v.end(); var p3 = new Vector(p2,v.angle+9*Math.PI/10, WIDTH/50).end(); var p4 = new Vector(p2,v.angle-9*Math.PI/10, WIDTH/50).end(); var p5 = v.middle(); if (!name) name=""+f2str(p2.x-p1.x)+", "+f2str(p2.y-p1.y)+")"; if(!color) color='#FCC'; ctx.beginPath(); ctx.lineWidth = 0.5; line(p1,p2); ctx.strokeStyle = color; ctx.stroke(); ctx.beginPath(); ctx.lineWidth = 0.5; moveTo(p3); lineTo(p4); lineTo(p2); lineTo(p3); ctx.fillStyle = color; ctx.fill(); ctx.strokeStyle = "#333"; ctx.stroke(); ctx.fillStyle = color; ctx.font = "12px Arial"; var pt2 =coord(p5); ctx.fillText(name,pt2.x+7,pt2.y-7); dot(v.start," ",color); } function dot(p,name,color) { ctx.beginPath(); var pt2 =coord(p); if (!name) name="("+f2str(p.x)+", "+f2str(p.y)+")"; if(!color) color='#CCC'; line(new Point(p.x-WIDTH/100,p.y),new Point(p.x+HEIGHT/100,p.y)); line(new Point(p.x,p.y-HEIGHT/100),new Point(p.x,p.y+HEIGHT/100)); ctx.lineWidth = 1; ctx.strokeStyle = color; ctx.stroke(); ctx.beginPath(); ctx.arc(pt2.x,pt2.y,5,0,2*Math.PI); ctx.fillStyle = color; ctx.fill(); ctx.stroke(); ctx.fillStyle = color; ctx.font = "12px Arial"; ctx.fillText(name,pt2.x+7,pt2.y-7); } function circle(p,radius,color) { ctx.beginPath(); var pt2 =coord(p); if(!color) color='#CCC'; line(new Point(p.x-WIDTH/100,p.y),new Point(p.x+HEIGHT/100,p.y)); line(new Point(p.x,p.y-HEIGHT/100),new Point(p.x,p.y+HEIGHT/100)); ctx.lineWidth = 1; ctx.strokeStyle = color; ctx.stroke(); ctx.beginPath(); ctx.arc(pt2.x,pt2.y,SCALE*radius,0,2*Math.PI); ctx.strokeStyle = color; ctx.stroke(); } clear(); function drawEulerSpiral(A, L1, L2, isCCW) { var a = A*Math.sqrt(2); var l1 = L1/a; var l2 = L2/a; var dx, dy; var s=l1; var ds = Math.abs(l2-l1)/10000; // 10000 points var M = new Point(0,0); var signe = ((isCCW)?1:-1); // for test and UI var firstDelta = null; var lastDelta = null; var testLenght = 0; // just for double check (total path lenght) ctx.beginPath(); ctx.lineWidth = 2; ctx.strokeStyle = "#F00"; while (s<l2) { dx = a * Math.cos(s*s) * ds; dy = signe * a * Math.sin(s*s) * ds; s += ds; var delta = new Point(dx,dy); var M2 = M.add(delta); // for test and UI testLenght += delta.norm(); if (!firstDelta) firstDelta = new Vector2(M, M2); lastDelta = new Vector2(M, M2); M=M2; lineTo(M2); } ctx.stroke(); var inRadius = null; if (L1!=0) inRadius = A*A/L1; var inStrRadius = (!inRadius)?"+inf":""+f2str(inRadius); var outRadius = null; if (L2!=0) outRadius = A*A/L2; var outStrRadius = (!outRadius)?"+inf":""+f2str(outRadius); firstDelta.lenght = 3; ray(firstDelta, "input (R="+inStrRadius+")", "#F88"); lastDelta.lenght = 3; ray(lastDelta, "output (R="+outStrRadius+")", "#88E"); var inAngle = (L1*L1)/(a*a); var outAngle = (L2*L2)/(a*a); if (inRadius) { firstDelta.lenght = Math.abs(inRadius); firstDelta.angle += ((inRadius<0)?-1:1) * signe * Math.PI/2; circle(firstDelta.end(),firstDelta.lenght,"#755"); } if (outRadius) { lastDelta.lenght = Math.abs(outRadius); lastDelta.angle += ((outRadius<0)?-1:1) * signe * Math.PI/2; circle(lastDelta.end(),lastDelta.lenght,"#557"); } } function ifcclothoidalarcsegment2d(Lenght, StartRadius, isCCW, isEntry, ClothoideConstant) { var A = ClothoideConstant; var L = Lenght; var startL; if (isEntry) { startL = 0; if (StartRadius!=null) startL = A*A / StartRadius; } else { startL = -L; if (StartRadius!=null) startL = -A*A / StartRadius; isCCW = !isCCW; } stopL = startL+L; drawEulerSpiral(A,startL,stopL,isCCW) } function onClear() { clear(); } function onClick() { var lenght = Number(document.getElementById("lenght").value); var StartRadius = Number(document.getElementById("StartRadius").value); if (isNaN(StartRadius) || StartRadius<=0 ) StartRadius = null; var isCCW = Number(document.getElementById("isCCW").value); var isEntry = Number( document.getElementById("isEntry").value); var ClothoideConstant = Number(document.getElementById("ClothoideConstant").value); ifcclothoidalarcsegment2d(lenght,StartRadius,isCCW,isEntry,ClothoideConstant); } onClick();   
 
 
 
 
 Share this article: Facebook Linkedin Twitter Copy linkLink copied Twitter Twitter 
 
 Discover our success stories. Go to page Our Work A software development project? Contact us! Go to page Subscribe to Our Newsletter Your email An error has occurred. Please try again later Thank you, your submission has been received. We won't share your information with third parties without your permission. Instagram Twitter Facebook Linkedin Toronto 647 477-3401 435 North Service Rd West Oakville, ON L6M 4X8  Gatineau-Ottawa 819 205-1800 60 Promenade du Portage Gatineau, QC J8X 2K1  Montreal 514 272-0979 110 Jean-Talon Street West Montreal, QC H2R 2X1  Brossard 514 272-0979 7005 Taschereau Blvd, #255 Brossard, QC J4Z 3P5    © 2020 Spiria Digital inc. Policy of confidentiality & Legal disclosure Cookies Settings var config = { apiKey: 'd71709ba0c4a358ff4e684ead6b274f07d08f296', product: 'PRO', initialState: "NOTIFY", layout: 'popup', position: 'LEFT', theme: 'LIGHT', setInnerHTML: true, consentCookieExpiry: 366, branding: { removeIcon:true, removeAbout: true, }, text : { title: 'Our site uses cookies', intro: 'Some of these cookies are essential for the proper operation of our site, while others help us understand how our site is used, support sharing on social networks and show messages based on your areas of interest.', accessibilityAlert : ' Warning: Some cookies require your attention', necessaryTitle : 'Essential cookies', necessaryDescription : 'These cookies are essential for the proper operation of our site. Without them, some pages won’t load properly, or at all. However, you can disable them in your web browser preferences.', notifyTitle: 'Your choice regarding cookies on this site', notifyDescription: 'We use cookies to optimize the operation of our site and to provide the best experience possible. <br><a href="/en/policy-confidentiality/#cookies">Learn More</a> or <button onclick="javascript:CookieControl.open();">manage</button> your cookies settings.', accept: 'Accept', settings: 'Settings', }, optionalCookies: [ { name: 'analytics', label: 'Statistics cookies', description: 'These cookies help us improve our web site by collecting information on what our visitors are interested in.', cookies: [], initialConsentState: "on", onAccept : function(){ ccAddTagAnalytics(); ccAddHotjar(); }, onRevoke: function(){ ccDisableHotjar(); window['ga-disable-UA-26609709-1'] = true; } },{ name: 'marketing', label: 'Marketing cookies', description: 'These cookies help us show marketing messages based on your areas of interest.', cookies: ['fr'], initialConsentState: "on", onAccept : function(){ ccAddTagAds(); ccAddDisqus(); }, onRevoke: function(){ ccDisableDisqus(); ccDisableTagAds(); } },{ name: 'social', label: 'Social network cookies', description: '<span class=\"first_p_txt\"/>These cookies support content sharing on social networks and adding comments on our blog articles.</span><span class=\"last_p_txt\"/>Spiria respects user privacy and data protection. For more detailed information on the cookies we use, see <a href=\"/en/policy-confidentiality/#cookies\">data protection</a>. You can access this settings panel at any time by clicking on the link “Cookies settings” at the bottom of every page on our site.</span>', cookies: [], initialConsentState: "on", onAccept : function(){ ccAddTagLinkedin(); !function(f,b,e,v,n,t,s) {if(f.fbq)return;n=f.fbq=function(){n.callMethod? n.callMethod.apply(n,arguments):n.queue.push(arguments)}; if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0'; n.queue=[];t=b.createElement(e);t.async=!0; t.src=v;s=b.getElementsByTagName(e); s.parentNode.insertBefore(t,s)}(window, document,'script', 'https://connect.facebook.net/en_US/fbevents.js'); fbq('init', '574101499358025'); fbq('track', 'PageView'); }, onRevoke: function(){ fbq('consent', 'revoke'); ccDisableTagLinkedin(); } } ], }; 