This page contains the source code of the page Cavendish Experiment Simulator.
The description of the graphic module JsGraph, the ControlPanels module and the Simulator module can be found here:
#INCLUDE JsGraph.inc #INCLUDE ControlPanel.inc #INCLUDE Sim.inc <style> .Wiki table.EventTable { width: 100%; font-size:smaller; } .Wiki table.EventTable td { width: 9.09%; text-align: right; white-space: nowrap; } .Wiki table.EventTable th { width: 9.09%; white-space: nowrap; } .Wiki table.EventTable td.c8 { color: red; } </style> <a id="App"></a> <jscript> // some global constants and functions var PI = Math.PI; var sin = Math.sin; var cos = Math.cos; var asin = Math.asin; var acos = Math.acos; var abs = Math.abs; function sqr(x) { return x*x; } var V2 = JsgVect2; // some vector functions inherited from graphic var Fmt = NumFormatter; //////////////////////////////////////////////////// // Scope function Scope( size, timeResolution ) { this.buffer = new Array(2*size); this.fill = 0; this.timeResolution = timeResolution; this.deltaTime = timeResolution; this.nextTime = timeResolution; this.maxValue = -360; this.minValue = 0; } Scope.prototype.Reset = function() { this.fill = 0; this.deltaTime = this.timeResolution; this.nextTime = this.timeResolution; this.maxValue = -360; this.minValue = 0; } Scope.prototype.Add = function( time, value ) { if (time >= this.nextTime) { this.buffer[this.fill] = value; this.fill++; if (this.fill >= this.buffer.length) { // compress buffer var limit = this.buffer.length / 2; for (var i = 0; i < limit; i++) { this.buffer[i] = (this.buffer[2*i] + this.buffer[2*i+1]) / 2; } this.fill = limit; this.deltaTime *= 2; } this.nextTime += this.deltaTime; if (value > this.maxValue) this.maxValue = value; if (value < this.minValue) this.minValue = value; } } Scope.prototype.Draw = function( g ) { // viewport has to be set as wished // window is changes by this function if (this.fill < 2) return; g.SetAlpha( 0.2 ); var winWidth = (this.fill - 1) * this.deltaTime; var winMin = this.minValue; var winMax = this.maxValue; var winHeight = 1.05 * (winMax - winMin); g.SetWindowWH( 0, winMin-0.025*winHeight, winWidth, winHeight ); g.NewPoly(); for (var i = 0; i < this.fill; i++) { g.AddPointToPoly( i*this.deltaTime, this.buffer[i] ); } g.DrawPoly(); g.Line( 0, 0, winWidth, 0 ); g.SetAlpha( 1 ); } //////////////////////////////////////////////////// // Meta data defines which Model properties are stored in a saved state or url var ModelMetaData = { Compact: false, DefaultPrec: 8, Properties: [ { Name: 'L1', Type: 'num', Default: 0.33 }, { Name: 'L2', Type: 'num', Default: 0.33 }, { Name: 'm1', Type: 'num', Default: 7.00 }, { Name: 'm2', Type: 'num', Default: 0.10 }, { Name: 'r1', Type: 'num', Default: 0.11 }, { Name: 'r2', Type: 'num', Default: 0.02 }, { Name: 'kt', Type: 'num', Default: 0.5e-9 }, { Name: 'kd', Type: 'num', Default: 5.0e-5 }, { Name: 'a0', Type: 'num', Default: 35 * PI / 180 }, { Name: 'av0', Type: 'num', Default: 0.00 }, { Name: 'theta0', Type: 'num', Default: 0.00 }, ], }; //////////////////////////////////////////////////// // the cavendish model var Model = { L1: 0.61, // m L2: 0.61, // m m1: 33.4, // kg m2: 0.78, // kg r1: 0.089, // m r2: 0.025, // m kt: 5.0e-6, // N m/rad kd: 5.0e-5, // N/(rad/s) a0: 14.4 * PI / 180, // zero tension angle in rad av0: 0, // initial angular speed in rad/s theta0: 0, // initial deflection angle Zoom: 1, MaxEvents: 9, // report each reverse and zero tension crossing event evtNum: 0, evtTypes: [], // 'turn N' or 'zero N' evtTimes: [], // event times angles: [], // angles at evtTimes graviForces: [], tensionForces: [], EventTableDom: null, // simulation parameters G: 6.674e-11, // gravitational constant // the following values are used by the simulator module Sim OldAccel: 0, // previous angular acceleration Accel: 0, // angular acceleration Speed: 0, // angular speed Pos: 0, // angular position alpha // internal values Fg: 0, // total gravitational tangential force Ft: 0, // string tension force Fd: 0, // damping force Ftot: 0, // sum of all forces Fg1: 0, // gravitational force between to neighboring masses amin: 0, // smaller collision angle limit amax: 0, // bigger collision angle limit canCollide: false, lastSpeed: 0, // reference speed for triggering a protocol event lastPos: 0, // reference angle for triggering a protocol event timeSpeed: 600, sim: null, // the simulator module graph: null, // the graphic module aScope: null, Create: function( sim ) { this.sim = sim; this.aScope = new Scope( 1000, 1 ); var me = this; this.graph = NewGraph2D( { Width: '100%', Height: '66%', DrawFunc: function(){ me.Draw(); }, OnClick: function() { startStop() }, AutoReset: false, AutoClear: false, AutoScalePix: true, } ); this.Update(); this.Reset(); }, Reset: function() { this.OldAccel = 0; this.Accel = 0; this.Speed = this.av0; this.Pos = this.a0 - this.theta0; this.CompAccel(); this.sim.InitStates( this ); this.lastSpeed = this.Speed; this.lastPos = this.Pos; this.evtNum = 0; this.evtTypes = []; this.evtTimes = []; this.angles = []; this.graviForces = []; this.tensionForces = []; this.WriteEventTable(); this.aScope.Reset(); }, Update: function() { // checks and limits the input parameters and computes the collision angles if (this.L1 < this.r1 + 0.001) this.L1 = this.r1 + 0.001; if (this.L2 < this.r2 + 0.001) this.L2 = this.r2 + 0.001; this.sim.TimeSpeed = this.timeSpeed; // check if collision is possible this.canCollide = (abs(this.L1-this.L2) < this.r1+this.r2-0.001); if (this.canCollide) { // angle from triange with 3 sides L1, L2 and r1+r2 this.amin = acos( (sqr(this.L1) + sqr(this.L2) - sqr(this.r1+this.r2)) / (2 * this.L1 * this.L2) ); this.amax = PI - this.amin; if (this.a0 < this.amin) this.a0 = this.amin; if (this.a0 > this.amax) this.a0 = this.amax; } else { if (this.a0 < -PI) this.a0 = -PI; if (this.a0 > PI) this.a0 = PI; } ControlPanels.Update(); this.Draw(); }, NextTimeStep: function() { // this function is called by the Simulator to update the current state this.CompAccel(); this.sim.CompNewStates( this ); this.HandleCollisions(); this.ProtocolEvents(); this.aScope.Add( this.sim.SimulTime, (this.a0-this.Pos) * 180 / PI ); }, CompAccel: function() { // computes all forces and the corresponding angular acceleration var r2 = [ -cos(this.Pos), sin(this.Pos) ]; var r2T = [ sin(this.Pos), cos(this.Pos) ]; var P1 = [ -this.L1, 0 ]; var P2 = [ -this.L2 * cos(this.Pos), this.L2 * sin(this.Pos) ]; var P3 = [ this.L1, 0 ]; var D1 = V2.Sub( P1, P2 ); var d1 = V2.Length( D1 ); var D1n = V2.Norm( D1 ); var D3 = V2.Sub( P3, P2 ); var d3 = V2.Length( D3 ); var D3n = V2.Norm( D3 ); var F = this.G * this.m1 * this.m2 / sqr(d1); this.Fg1 = F; var Fg1 = V2.ScalarProd( V2.Scale( D1n, F ), r2T ); var F = this.G * this.m1 * this.m2 / sqr(d3); var Fg2 = V2.ScalarProd( V2.Scale( D3n, F ), r2T ); this.Fg = 2 * (Fg1 + Fg2); this.Ft = this.kt * (this.a0 - this.Pos) / this.L2; this.Fd = - this.kd * this.Speed; this.Ftot = this.Fg + this.Ft + this.Fd; this.Accel = this.Ftot / (2 * this.m2 * this.L2); }, HandleCollisions: function() { // detects collisions of masses and reverts the turning on collisions. // It implements elastic collisions. if (!this.canCollide) return; if (this.Pos < this.amin) { var penetration = this.amin - this.Pos; this.Pos = this.amin + penetration; this.Speed *= -1; } else if (this.Pos > this.amax) { var penetration = this.Pos - this.amax; this.Pos = this.amax - penetration; this.Speed *= -1; } }, ProtocolEvents: function() { // detects rotation change and zero angle events // and stores time and angle of the event for later output in Draw() // check direction reverse var nEvents = this.evtTimes.length; if (this.Speed * this.lastSpeed < 0) { this.evtNum++; if (nEvents >= this.MaxEvents) { this.evtTypes[nEvents-1] = this.evtNum + ' turn'; this.evtTimes[nEvents-1] = this.sim.SimulTime; this.angles[nEvents-1] = (this.a0-this.Pos) * 180 / PI; this.graviForces[nEvents-1] = abs(this.Fg); this.tensionForces[nEvents-1] = abs(this.Ft); } else { this.evtTypes.push( this.evtNum + ' turn' ); this.evtTimes.push( this.sim.SimulTime ); this.angles.push( (this.a0-this.Pos) * 180 / PI ); this.graviForces.push( abs(this.Fg) ); this.tensionForces.push( abs(this.Ft) ); } } // check zero tension angle crossing var lastZeroPos = this.lastPos - this.a0; var zeroPos = this.Pos - this.a0; if (lastZeroPos * zeroPos < 0) { this.evtNum++; if (nEvents >= this.MaxEvents) { this.evtTypes[nEvents-1] = this.evtNum + ' zero'; this.evtTimes[nEvents-1] = this.sim.SimulTime; this.angles[nEvents-1] = (this.a0-this.Pos) * 180 / PI; this.graviForces[nEvents-1] = abs(this.Fg); this.tensionForces[nEvents-1] = abs(this.Ft); } else { this.evtTypes.push( this.evtNum + ' zero' ); this.evtTimes.push( this.sim.SimulTime ); this.angles.push( (this.a0-this.Pos) * 180 / PI ); this.graviForces.push( abs(this.Fg) ); this.tensionForces.push( abs(this.Ft) ); } } this.lastSpeed = this.Speed; this.lastPos = this.Pos; }, Draw: function() { // this function is called by the Simulator Module to draw the current state var g = this.graph; g.Reset(); g.SetAngleMeasure( 'rad' ); // draw scope g.SetLineAttr( 'red', 2 ); this.aScope.Draw( g ); // compute the scene size and set the window accordingly var l1 = this.L1 + this.r1; var l2 = this.L2 + this.r2; var lmin = l1 > l2 ? l1 : l2; var winXCenter = 0; var winYCenter = 0; var winWidth = 1.1 * 2 * lmin / this.Zoom; var winHeight = 0; // auto g.MapWindow( winXCenter, winYCenter, winWidth, winHeight ); // zero tension fixed bar g.SetAlpha( 0.2 ); g.SetLineAttr( 'black', 4 ); var x = (this.L2 - this.r2) * cos( this.a0 ); var y = (this.L2 - this.r2) * sin( this.a0 ); g.Line( -x, y, x, -y ); var x = this.L2 * cos( this.a0 ); var y = this.L2 * sin( this.a0 ); g.SetAreaAttr( 'lightblue', 'blue', 2 ); g.Circle( -x, y, this.r2, 3 ); g.Circle( x, -y, this.r2, 3 ); g.SetAlpha( 1 ); // angle arc var dir = this.Pos-this.a0 < 0 ? 1 : -1; g.SetLineAttr( 'red', 3 ); g.Arc( 0, 0, dir*this.L2*0.8, PI-this.a0, PI-this.Pos, 1 ); g.Arc( 0, 0, dir*this.L2*0.8, -this.a0, -this.Pos, 1 ); // suspension bar g.SetLineAttr( 'black', 4 ); var x = (this.L2 - this.r2) * cos( this.Pos ); var y = (this.L2 - this.r2) * sin( this.Pos ); g.Line( -x, y, x, -y ); // draw pivot point g.SetMarkerAttr( 'Circle', 8, 'black', 'yellow', 2 ); g.Marker( 0, 0 ); // fixed masses g.SetAreaAttr( 'gray', 'black', 2 ); g.Circle( -this.L1, 0, this.r1, 3 ); g.Circle( this.L1, 0, this.r1, 3 ); // suspended masses var x = this.L2 * cos( this.Pos ); var y = this.L2 * sin( this.Pos ); g.SetAreaAttr( 'lightblue', 'blue', 2 ); g.Circle( -x, y, this.r2, 3 ); g.Circle( x, -y, this.r2, 3 ); function data( line, text ) { g.Text( text, g.ScalePix(10), g.VpInnerHeight - g.ScalePix(20)*line - g.ScalePix(10) ); } // plot values in the graphic viewport g.SelectTrans( 'viewport' ); g.SetTextAttr( 'Courier', 14, 'black', 'bold', 'normal', 'left', 'bottom' ); data( 5, 'Time = ' + this.formatTime(this.sim.SimulTime) ); data( 4, 'Theta = ' + Fmt.Format((this.a0-this.Pos) * 180 / PI,'std',5,'°') ); data( 3, 'Fg1 = ' + Fmt.Format(abs(this.Fg1),'unit',5,' N') ); data( 2, 'Fg = ' + Fmt.Format(abs(this.Fg),'unit',5,' N') ); data( 1, 'Ft = ' + Fmt.Format(abs(this.Ft),'unit',5,' N') ); data( 0, 'Speed = ' + (this.Speed < 0 ? '-' : '+') + Fmt.Format(abs(this.Speed)*180/PI*3600,'fix0',3,'°/h') ); this.WriteEventTable(); }, formatTime: function( t ) { // format t in seconds to format hh:mm:ss return NumFormatter.Format( t/3600, 'hms', 4 ); }, WriteEventTable: function() { if (!this.EventTableDom) return; var s = ''; s += '<tr><th>Event</th>'; var nEvents = this.evtTimes.length; for (var i = 0; i < this.MaxEvents; i++) { if (i < nEvents) { s = s + '<td class="c'+i+'">' + this.evtTypes[i] + '</td>'; } else { s = s + '<td> </td>'; } } s += '<tr><th>Time [hms]</th>'; var nEvents = this.evtTimes.length; for (var i = 0; i < this.MaxEvents; i++) { if (i < nEvents) { s = s + '<td class="c'+i+'">' + this.formatTime(this.evtTimes[i]) + '</td>'; } else { s = s + '<td> </td>'; } } s += '<tr><tr><th>Time [s]</th>'; for (var i = 0; i < this.MaxEvents; i++) { if (i < nEvents) { s = s + '<td class="c'+i+'">' + this.evtTimes[i].toFixed(0) + '</td>'; } else { s = s + '<td> </td>'; } } var lastTime = 0; s += '<tr><tr><th>ΔTime [s]</th>'; for (var i = 0; i < this.MaxEvents; i++) { if (i < nEvents) { s = s + '<td class="c'+i+'">' + (this.evtTimes[i]-lastTime).toFixed(0) + '</td>'; lastTime = this.evtTimes[i]; } else { s = s + '<td> </td>'; } } s += '<tr><tr><th>Angle Θ [°]</th>'; for (var i = 0; i < this.MaxEvents; i++) { if (i < nEvents) { s = s + '<td class="c'+i+'">' + Fmt.Format(this.angles[i],'fix0',4) + '</td>'; } else { s = s + '<td> </td>'; } } s += '<tr><tr><th>Fg [nN]</th>'; for (var i = 0; i < this.MaxEvents; i++) { if (i < nEvents) { s = s + '<td class="c'+i+'">' + Fmt.Format(this.graviForces[i]*1e9,'std',5,'','calc') + '</td>'; } else { s = s + '<td> </td>'; } } s += '<tr><tr><th>Ft [nN]</th>'; for (var i = 0; i < this.MaxEvents; i++) { if (i < nEvents) { s = s + '<td class="c'+i+'">' + Fmt.Format(this.tensionForces[i]*1e9,'std',5,'','calc') + '</td>'; } else { s = s + '<td> </td>'; } } s = '<table class="grid EventTable">' + s + '</tr></table>'; xInnerHTML( this.EventTableDom, s ); }, }; /////////////////////////////////////////////// // Simulator var Simulator = new Sim( { SimObj: Model, TimeSpeed: 1000, TimeStep: 0.5, ResetFuncs: function(sim) { sim.SimObj.Reset(); }, TimeStepFuncs: function(sim) { sim.SimObj.NextTimeStep(); }, FrameFuncs: function(sim) { sim.SimObj.Draw(); }, } ); Model.Create( Simulator ); ////////////////////////////////////////////// xOnLoad( function() { Model.EventTableDom = xElement('EventTable'); Model.Update(); Model.WriteEventTable(); } ); function start() { Simulator.Run( true ); } function startStop() { if (Simulator.Running) { Simulator.Stop(); } else { Simulator.Run( true ); } } // state save and restoring function ClearAppStatePanel() { if (Model.StateDisplay) { Model.StateDisplay.value = ''; Model.StateDisplay.focus(); } } DataX.AssignApp( 'CavendishModel', Model, ModelMetaData, function(){ResetApp(false);}, UpdateAndStart ); DataX.AssignSaveRestoreDomObj( 'SaveRestorePanel' ); DataX.SetupUrlStateHandler( 'App' ); function ResetApp(update) { update = xDefBool( update, true ); SetAppState( 'CavendishModel = { "L1": 0.61, "L2": 0.61, "m1": 33.4, "m2": 0.78, "r1": 0.089, "r2": 0.025, "kt": 5e-6, "kd": 5e-6, "a0": 0.251327, "av0": 0, "theta0": 0 }' ); if (update) UpdateAndStart(); } function SetAppState( jsonStr ) { DataX.JsonToAppState( jsonStr ); UpdateAndStart(); } function SetAppStateExampleTension() { SetAppState( 'CavendishModel = { "L1": 0.33, "L2": 0.33, "m1": 0, "m2": 0.1, "r1": 0, "r2": 0.02, "kt": 2e-8, "kd": 0.000005, "a0": 0.61086524, "av0": 0, "theta0": 0.017453293 }' ); location.hash = '#App'; } function SetAppStateExampleG() { SetAppState( 'CavendishModel = { "L1": 0.33, "L2": 0.33, "m1": 7, "m2": 0.1, "r1": 0.11, "r2": 0.02, "kt": 2e-8, "kd": 0.000005, "a0": 0.61086524, "av0": 0 }' ); location.hash = '#App'; } function SetAppStateHarward() { SetAppState( 'CavendishModel = { "L1": 0.067, "L2": 0.05, "m1": 1.5, "m2": 0.0383, "r1": 0.0316, "r2": 0.0093, "kt": 3.1e-8, "kd": 0.000005, "a0": 0.73303829, "av0": 0, "theta0": 0 }' ); location.hash = '#App'; } // create panels function UpdateAndStart() { // this function is called when the user updates sliders or values Model.Update(); start(); } </jscript> {{scroll}} <jscript> //////////////////////////////////////////////////// // Control Panels ControlPanels.NewSliderPanel( { ModelRef: 'Model', NCols: 1, ValuePos: 'left', OnModelChange: function(field){ Model.Update(field); }, Format: 'fix0', Digits: 0, ReadOnly: false, PanelFormat: 'InputShortWidth' } ).AddValueSliderField( { Name: 'timeSpeed', Label: 'Speed', Color: 'blue', LogScale: true, LowerLimit: 1, UpperLimit: 10000, Min: 0, Max: 4, Inc: 100, SnapTo: 3, } ).AddValueSliderField( { Name: 'Zoom', Label: 'Zoom', Color: 'black', Digits: 2, Min: 0.1, Max: 1, Inc: 0.1, LowerLimit: 0.01, UpperLimit: 10, } ).Render(); </jscript> {{end scroll}} {{scroll}} <jscript> ControlPanels.NewPanel( { ModelRef: 'Model', OnModelChange: function(field){ Model.Update(field); start(); }, PanelFormat: 'InputNormalWidth', NCols: 2, Format: 'std', Digits: 5 } ).AddTextField( { Name: 'm1', Label: 'm<sub>1</sub>', Units: 'kg', LowerLimit: 0, Inc: 0.01, } ).AddTextField( { Name: 'm2', Label: 'm<sub>2</sub>', Units: 'kg', LowerLimit: 0, Inc: 0.01, } ).AddTextField( { Name: 'r1', Label: 'r<sub>1</sub>', Units: 'cm', Mult: 1/100, LowerLimit: 0, Inc: 1, } ).AddTextField( { Name: 'r2', Label: 'r<sub>2</sub>', Units: 'cm', Mult: 1/100, LowerLimit: 0, Inc: 1, } ).AddTextField( { Name: 'L1', Label: 'L<sub>1</sub>', Units: 'cm', Mult: 1/100, Inc: 1, } ).AddTextField( { Name: 'L2', Label: 'L<sub>2</sub>', Units: 'cm', Mult: 1/100, Inc: 1, } ).AddTextField( { Name: 'kt', Label: 'k<sub>t</sub>', Units: 'μN·m/rad', Mult: 1/1e6, LowerLimit: 0, Inc: 1, } ).AddTextField( { Name: 'kd', Label: 'k<sub>d</sub>', Units: 'μN·s/rad', Mult: 1/1e6, LowerLimit: 0, Inc: 1, } ).AddTextField( { Name: 'a0', Label: 'α<sub>0</sub>', Units: '°', Mult: PI/180, Inc: 1, } ).AddTextField( { Name: 'theta0', Label: 'θ<sub>0</sub>', Units: '°', Mult: PI/180, Inc: 1, } ).Render(); ////////////////////////////////////////////////////// start(); </jscript> {{end scroll}}