WaBis

walter.bislins.ch

Source Code: Cavendish Experiment Simulator

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>&nbsp;</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>&nbsp;</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>&nbsp;</td>';
      }
    }

    var lastTime = 0;
    s += '<tr><tr><th>&Delta;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>&nbsp;</td>';
      }
    }

    s += '<tr><tr><th>Angle &Theta; [°]</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>&nbsp;</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>&nbsp;</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>&nbsp;</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: '&mu;N·m/rad',
    Mult: 1/1e6,
    LowerLimit: 0,
    Inc: 1,
  }

).AddTextField( {
    Name: 'kd',
    Label: 'k<sub>d</sub>',
    Units: '&mu;N·s/rad',
    Mult: 1/1e6,
    LowerLimit: 0,
    Inc: 1,
  }

).AddTextField( {
    Name: 'a0',
    Label: '&alpha;<sub>0</sub>',
    Units: '°',
    Mult: PI/180,
    Inc: 1,
  }

).AddTextField( {
    Name: 'theta0',
    Label: '&theta;<sub>0</sub>',
    Units: '°',
    Mult: PI/180,
    Inc: 1,
  }

).Render();

//////////////////////////////////////////////////////

start();

</jscript>

{{end scroll}}



More Page Infos / Sitemap
Created Thursday, July 26, 2018
Scroll to Top of Page
Changed Wednesday, August 8, 2018