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;

// 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-6 },
    { 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.33,   // m
  L2: 0.33,   // m
  m1: 7.0,    // kg
  m2: 0.1,    // kg
  r1: 0.11,   // m
  r2: 0.02,   // m
  kt: 0.5e-9, // Nm/rad
  kd: 5.0e-6, // N/(rad/s)
  a0: 35 * 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
  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

  sim: null,     // the simulator module
  graph: null,   // the graphic module

  Create: function( sim ) {
    this.sim = sim;
    var me = this;
    this.graph = NewGraph2D( {
      Width: '100%',
      Height: '66%',
      DrawFunc: function(){ me.Draw(); },
      OnClick: function() { startStop() },
      AutoReset: false,
      AutoClear: false,
    } );
    this.UpdatePara();
    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.evtTimes = [];
    this.angles = [];
    this.graviForces = [];
    this.tensionForces = [];
    this.WriteEventTable();
  },

  UpdatePara: function() {
    // checks and limits the input parameters and computes the collision angles

    if (this.r1 < 0) this.r1 = 0;
    if (this.r2 < 0) this.r2 = 0;
    if (this.m1 < 0) this.m1 = 0;
    if (this.m2 < 0) this.m2 = 0;
    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;
    if (this.kt < 0) this.kt = 0;
    if (this.kd < 0) this.kd = 0;
    if (this.sim.TimeSpeed > 10000) this.sim.TimeSpeed = 10000;

    // 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;
    }
  },

  Update: function() {
    // this function is called by the Simulator to update the current state
    this.CompAccel();
    this.sim.CompNewStates( this );
    this.HandleCollisions();
    this.ProtocolEvents();
  },

  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) {
      if (nEvents >= this.MaxEvents) {
        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.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) {
      if (nEvents >= this.MaxEvents) {
        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.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' );

    // 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 );

    // draw axes
    g.SetLineAttr( '#ddd', 1 );
    g.Axes();

    // 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', 2 );
    g.Arc( 0, 0, dir*this.L2/3, PI-this.a0, PI-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 );

    // 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, 10, g.VpInnerHeight - 20*line - 10 );
    }

    // plot values in the graphic viewport
    g.SelectTrans( 'viewport' );
    g.SetTextAttr( 'Courier', 14, 'black', 'normal', '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
    t /= 3600;
    var ts = '';
    var v = Math.floor( t );
    ts += v.toFixed(0);
    t = (t - v) * 60;
    v = Math.floor( t );
    ts += ':' + (v < 10 ? '0' : '') + v.toFixed(0);
    t = (t - v) * 60;
    v = Math.floor( t );
    ts += ':' + (v < 10 ? '0' : '') + v.toFixed(0);
    return ts;
  },

  WriteEventTable: function() {

    if (!this.EventTableDom) return;
    var 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>';
      }
    }
    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',3) + '</td>';
      } else {
        s = s + '<td>&nbsp;</td>';
      }
    }
    s += '<tr><tr><th>Angle &Theta; [rad]</th>';
    for (var i = 0; i < this.MaxEvents; i++) {
      if (i < nEvents) {
        s = s + '<td class="c'+i+'">' + Fmt.Format(this.angles[i]*PI/180,'fix0',5) + '</td>';
      } else {
        s = s + '<td>&nbsp;</td>';
      }
    }
    s += '<tr><tr><th>Fg [N]</th>';
    for (var i = 0; i < this.MaxEvents; i++) {
      if (i < nEvents) {
        s = s + '<td class="c'+i+'">' + Fmt.Format(this.graviForces[i],'eng',4,'','calc') + '</td>';
      } else {
        s = s + '<td>&nbsp;</td>';
      }
    }
    s += '<tr><tr><th>Ft [N]</th>';
    for (var i = 0; i < this.MaxEvents; i++) {
      if (i < nEvents) {
        s = s + '<td class="c'+i+'">' + Fmt.Format(this.tensionForces[i],'eng',4,'','calc') + '</td>';
      } else {
        s = s + '<td>&nbsp;</td>';
      }
    }
    s = '<table class="grid EventTable">' + s + '</tr></table>';
    xInnerHTML( this.EventTableDom, s );
  },

};

var Simulator = new Sim( {
  SimObj:        Model,
  TimeSpeed:     1000,
  TimeStep:      0.5,
  ResetFuncs:    function(sim) { sim.SimObj.Reset(); },
  TimeStepFuncs: function(sim) { sim.SimObj.Update(); },
  FrameFuncs:    function(sim) { sim.SimObj.Draw(); },
} );

Model.Create( Simulator );

xOnLoad(
  function() {
    Model.EventTableDom = xElement('EventTable');
    Model.WriteEventTable();
    UpdateAll();
  }
);

function start() {
  Simulator.Run( true );
}

function startStop() {
  if (Simulator.Running) {
    Simulator.Stop();
  } else {
    Simulator.Run( true );
  }
}

start();

// state save and restoring

function ClearAppStatePanel() {
  if (Model.StateDisplay) {
    Model.StateDisplay.value = '';
    Model.StateDisplay.focus();
  }
}

DataX.AssignApp( 'CavendishModel', Model, ModelMetaData, function(){ResetApp(false);}, UpdateAll );
DataX.AssignSaveRestoreDomObj( 'SaveRestorePanel' );
DataX.SetupUrlStateHandler( 'App' );

function ResetApp(update) {
  update = xDefBool( update, true );
  SetAppState( 
    'CavendishModel = { "L1": 0.33, "L2": 0.33, "m1": 7, "m2": 0.1, "r1": 0.11, "r2": 0.02, "kt": 5e-10, "kd": 0.000005, "a0": 0.61086524, "av0": 0, "theta0": 0 }' );
  if (update) UpdateAll();
}

function SetAppState( jsonStr ) {
  DataX.JsonToAppState( jsonStr );
  UpdateAll();
}

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.17453293 }' );
  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';
}

// create panels

function UpdateAll() {
  // this function is called when the user updates sliders or values
  Model.UpdatePara();
  ControlPanels.Update();
  Model.Draw();
  start();
}

function UpdateSimul() {
  // this function is called when the user updates values which don't need a restart
  if (Simulator.TimeSpeed > 10000) Simulator.TimeSpeed = 10000;
  if (Simulator.TimeSpeed < 0) Simulator.TimeSpeed = 0;
  ControlPanels.Update();
}

</jscript>

{{scroll}}

<jscript>

ControlPanels.NewSliderPanel( {
  ModelRef: 'Simulator',
  NCols: 1,
  ValuePos: 'left',
  OnModelChange: UpdateSimul,
  Format: 'fix0',
  Digits: 0,
  ReadOnly: false,
  PanelFormat: 'InputShortWidth'

} ).AddValueSliderField( {
  Name: 'TimeSpeed',
  Label: 'Speed',
  Color: 'blue',
  Min: 1,
  Max: 10000,
  Inc: 100,

} ).AddValueSliderField( {
  Name: 'Model.Zoom',
  Label: 'Zoom',
  Color: 'black',
  Digits: 2,
  Min: 0.5,
  Max: 1,
  Inc: 0.1,

} ).Render();


</jscript>

{{end scroll}}

{{scroll}}

<jscript>

ControlPanels.NewPanel( {
    ModelRef: 'Model',
    OnModelChange: UpdateAll,
    PanelFormat: 'InputNormalWidth',
    NCols: 2,
    Format: 'std',
    Digits: 5
  }

).AddTextField( {
    Name: 'm1',
    Label: 'm<sub>1</sub>',
    Units: 'kg',
    Inc: 0.01,
  }

).AddTextField( {
    Name: 'm2',
    Label: 'm<sub>2</sub>',
    Units: 'kg',
    Inc: 0.01,
  }

).AddTextField( {
    Name: 'r1',
    Label: 'r<sub>1</sub>',
    Units: 'cm',
    Mult: 1/100,
    Inc: 1,
  }

).AddTextField( {
    Name: 'r2',
    Label: 'r<sub>2</sub>',
    Units: 'cm',
    Mult: 1/100,
    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: 'nN·m/rad',
    Mult: 1/1e9,
    Inc: 1,
  }

).AddTextField( {
    Name: 'kd',
    Label: 'k<sub>d</sub>',
    Units: '&mu;N·s/rad',
    Mult: 1/1e6,
    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,
  }
/*
).AddTextField( {
    Name: 'Simulator.TimeStep',
    Label: 'TimeStep',
    Units: 'ms',
    Mult: 1/1000,
  }
*/
).Render();
</jscript>

{{end scroll}}



Weitere Infos zur Seite
Erzeugt Thursday, July 26, 2018
von wabis
Zum Seitenanfang
Geändert Wednesday, August 8, 2018
von wabis