#!/usr/bin/wish -f ## ## SCRIPT: tkSimulateSinglePendulumNoFriction.tk ## ## PURPOSE: This Tk GUI script solves the second-order differential ## equation for the oscillations of a single pendulum by ## using numerical integration (the Runge-Kutta method). ## ## The ODE (ordinary differential equation) for a single, ## fixed-pivot-point pendulum arm with no frictional forces is ## ## D(D(u)) = -g/L * sin(u) ## ## where D represents the time-derivative operator d/dt, ## and t represents the time independent variable, ## and u represents the angular displacement of the ## pendulum arm from vertical, ## and g is the acceleration due to gravity (on Earth or ## the Moon or Mars or a mountain-top or whatever), ## and L is the length of the pendulum arm (the distance ## to a heavy mass at the end of the arm, where ## the mass is so great that the weight of the ## arm is negligible --- or L is the distance ## from the fixed-pivot-point to the center-of-mass ## of the arm and the mass at the end of the arm). ## ## This 'nonlinear' form of the right-hand-side allows for ## simulating a widely-swinging (significantly displaced) ## pendulum. ## ## If one considers a pendulum swinging with small angular ## displacement, we get a good approximation to the ## motion by using the ODE ## ## D(D(u)) = -g/L * u ## ## because u is a good approximation to sin(u) when u ## is small. The solution of this ODE is of the form ## ## A * sin(w*t) + B * cos(w*t) ## ## where w = sqrt(g/L) and where the constants A and B ## can be determined from two initial conditions (inital ## angular displacement and initial angular velocity). ## ## '2pi/w' is the period T of the oscillation where ## T is in units like seconds per cycle. ## ## The reciprocal of T is the cycles per second --- the ## frequency. So the frequency, f, is w/2pi. Thus the ## solution can be written ## ## A * sin(2pi*f*t) + B * cos(2pi*f*t) ## or ## A * sin(2pi*t/T) + B * cos(2pi*t/T) ## ## An expression of the form ## A * sin(k*t) + B * cos(k*t) ## can be written in the form ## C * sin(k*t + D) ## where the constant D is called a 'phase angle'. ## Hence the solutions to the 'linear' pendulum ## equations are sinusoidal. ## ## The solutions of the nonlinear pendulum equation ## can be expected to be similar to a sinusoidal function ## --- but with the graph against time somewhat distorted. ## ################ ## GUI FEATURES: ## ## The GUI allows the user to enter various values ## for g and L. ## ## (Note that the equation for angular acceleration does ## not involve mass --- the mass factors cancelled out ## during derivation of the equation. So the angular ## motion of the (friction-less) pendulum should be the ## same whether the mass is 10 kg or 20 kg, for example.) ## ## The GUI also allows the user to enter parameters ## for the solver process: ## - an initial angular displacement from vertical ## - an initial angular velocity (typically zero) ## - an end-time for the end of the solution process ## - a time-step, h, for the solution process. ## ## Following a solution run, the GUI also allows the ## user to start (and stop) an animation of a pendulum ## drawn on a Tk canvas. ## ## The animation is shown on a rectangular Tk 'canvas' widget ## by using 'create line' and 'create oval' and 'delete' ## commands on the canvas. ## ################################################# ## METHOD - MATH MODELLING OF THE PENDULUM MOTION: ## ## We convert the single 'second order' differential equation ## ## D(D(u)) = -g/L * sin(u) ## ## to two 'first order' differential equations with ## u (= u1) and D(u) (= u2) as the functions of t ## to be generated by integrating 2 differential eqns: ## ## D(u1) = u2 ## D(u2) = -g/L * sin(u1) ## ## for initial conditions u1=A and u2=B, where ## A is an initial angle and B is an initial angular velocity. ## ## The common way of expressing these kinds of systems of ## first order differential equations in compact, general form is ## ## D(u) = f(t,u) ## ## where u and f are N-dimensional vectors. ## ## This is a compact way of expressing a system of scalar ## differential equations: ## ## D(u1) = f1(t,u1,...,uN) ## D(u2) = f2(t,u1,...,uN) ## ...... ## D(uN) = fN(t,u1,...,uN) ## ## In the case of these pendulum equations, N=2, and ## we can think of solving for the unknown function vector ## (u1(t),u2(t)) where the right-hand-side (RHS) of the ## two equations above can be thought of as a special ## case of a more general user-specified function vector ## (f1(t,u1,u2),f2(t,u1,u2)) where ## ## f1(t,u1,u2) = u2 ## f2(t,u1,u2) = -g/L * sin(u1). ## ## We use the popular Runge-Kutta 4th order method (RK4) to ## perform the numerical integration for a user-specified ## time step, h. ## ## We basically use two procs to perform the integration ## steps: ## - a proc to perform the RK4 integration for N=2--- ## giving the values of (u1,u2) for each time step ## ## - a proc to evaluate the RHS function: (f1,f2) ## for specified values of t,u1,u2. ## ## The latter proc is called several times by the former proc ## for each time step. ## ################################################################### ## METHOD - PLOTTING THE ANIMATION ON THE TK CANVAS: ## ## After a solution, we have the solution functions ## u1 and u2 for a sequence of equally-spaced time values. ## ## We use function u1 to do the animation. ## ## For each time value, t(i), the pendulum is drawn ## as a simple ## - line-segment representing the 'arm' of the pendulum ## - color-filled circle representing the mass at the ## end of the (essentially weight-less) arm. ## ## The GUI provides 2 buttons by which the user can specify ## the 2 colors for: ## - the canvas background ## - the pendulum components (line and circle). ## ## An 'animate' proc performs the pendulum animation when ## the user clicks on the 'Start' radiobutton of the GUI. ## ## This animate proc uses the 'world-coordinates' --- the ## values of angle u1 --- to draw the swinging pendulum within ## an area of about 2*L by 2*L in world coordinates, where ## L is the length of the pendulum arm. ## ## We think of the pivot-point of the pendulum as being at ## the origin --- (0.0,0.0). We use the angle u1 to compute ## the x,y coordinates of the end of the pendulum arm (and ## center of the mass, represented by a circle). ## ## The 2Lx2L area allows for the pendulum to swing to extremes ## --- a horizontal arm to the left and right --- and ## a vertical arm below (zero degrees) the pivot-point ## or above (plus or minus 180 degrees) the pivot-point. ## ## A proc is provided which maps the plot area limits in ## world coordinates --- say ## UpperLeftCorner: (-L,+L) ## LowerRightCorner: (+L,-L) ## to the corners of the plot area in pixel coordinates: ## UpperLeftCorner: (0,0) ## LowerRightCorner: (ImageWidthPx,ImageHeightPx) ## ## We use a value a little larger than L for the world coordinate ## limits --- to allow for a little margin around the swinging ## pendulum. ## ## To get a square image area, we use ImageWidthPx=ImageHeightPx ## and we determine this number of pixels by allowing the user ## to specify the integer value in an entry widget on the GUI. ## ## The animate proc uses 2 procs --- Xwc2Xpx and Ywc2Ypx --- ## to convert the world coordinates of each point --- such as ## the end-points of the pendulum arm --- to pixel coordinates. ## ## The pixel-coordinates are used in the 'create line' ## and 'create oval' commands to redraw the pendulum-arm ## and mass (represented by a circle) for each time step. ## ##+############## ## THE GUI LAYOUT: ## ## Recall that the Tk GUI should allow the user to specify ## g, L, initial-angle, initial-angular-velocity, ## time-step-size, end-time. ## ## There is to be a 'Solve' button to perform a solution ## when the user is ready to use these parameters. ## ## There are also to be 2 buttons by which to call up an ## RGB-color-selector GUI by which to specify the 2 colors ## for the animation drawing on the canvas. ## ## A 'ShowList' button can be used to show the list of ## solution values --- triplets t(i), u1(t(i)), u2(t(i)) --- ## in a popup window. ## ## We allow the user to specify the two intial values in ## degrees, rather than radians --- initial angular displacement ## in degrees and initial angular velocity in degrees/sec. ## These two values are converted to radians for easy and ## efficient use in the Tcl sin() function of the solve process. ## ## We may add 2 columns to the list output, by showing ## u1 and u2 in degrees, as well as in radians. ## ## In addition, on the GUI, there are to be 'Start' and 'Stop' ## radiobuttons to start and stop an animation run. ## ## To allow the user to speed-up or slow-down the animation, ## there may be a Tk widget ('entry' or 'scale') by which to specify ## a wait-time (in millisecs) between computing and displaying ## each new pendulum position. This would be an alternative to ## using a hard-coded wait-time value. ## ## One way the user can specify these parameters is indicated by ## the following 'sketch' of the GUI: ## ## FRAMEnames ## VVVVVVVVVV ## ------------------------------------------------------------------------------------------ ## Simulate Single Pendulum -- No Friction - an animation ## [window title] ## ------------------------------------------------------------------------------------------ ## ## .fRbuttons {Exit} {Help} {Solve} {Show {Reset Animate: O Start O Stop {Pendulum {Background ## List} Parms} Color} Color} ## ## .fRrhs [ ........ Angular acceleration expression goes here, in a label widget .......... ] ## (This could be an entry widget, someday, to allow for changes in the math expression.) ## ## .fRparms g (gravitational acceleration): 9.8__ Length of pendulum arm: 10.0__ Image square (pixels): 300__ ## ## .fRinit Initial Anglular Displacement (degrees): 8____ Initial Angular Velocity (degrees/sec): 0.0___ ## ## .fRtimes Solve End Time (secs): 20__ Solve Step Size (secs): 0.1___ ## ## .fRmsg [ .......... Messages go here, in a label widget .......................... ] ## ## .fRcanvas |------------------------------------------------------------------------| ## | | ## | [This non-scrollable canvas contains the square animation image | ## | area --- centered at the top of the canvas widget. | ## | | ## | | ## | | ## | | ## |------------------------------------------------------------------------| ## ## ------------------------------------------------------------------- ## ## In the above sketch of the GUI: ## ## SQUARE BRACKETS indicate a comment (not to be placed on the GUI). ## BRACES indicate a Tk 'button' widget. ## UNDERSCORES indicate a Tk 'entry' widget. ## A COLON indicates that the text before the colon is on a 'label' widget. ## CAPITAL-O indicates a Tk 'radiobutton' widget. ## CAPITAL-X indicates a Tk 'checkbutton' widget (if any). ## Vertical bars (and horizontal hyphens) outline a 'canvas' widget. ## ## If there are scrollbars: ## Less-than and greater-than signs indicate the left and right ends of a horizontal 'scrollbar'. ## Capital-V and Capital-A letters indicate the bottom and top ends of a vertical 'scrollbar'. ## ##+############## ## GUI components: ## ## From the GUI 'sketch' above, it is seen that the GUI consists of about ## ## - 7 button widgets ## - 10 label widgets ## - 7 entry widgets ## - 1 canvas widget with no scrollbars ## - 2 radiobutton widgets in 1 group ## - 0 scale widgets (but may use scale widgets in place of some entry widgets) ## - 0 checkbutton widgets ## - 0 listbox widgets ## - 0 text widgets ## ##+######################################################################## ## 'CANONICAL' STRUCTURE OF THIS TK CODE: ## ## 0) Set general window & widget parms (win-name, win-position, ## win-color-scheme, fonts, widget-geometry-parms, win-size-control). ## ## 1a) Define ALL frames (and sub-frames, if any). ## 1b) Pack ALL frames and sub-frames. ## ## 2) Define all widgets in the frames. Pack them. ## ## 3) Define keyboard or mouse/touchpad/touch-sensitive-screen action ## BINDINGS, if needed. ## ## 4) Define PROCS, if needed. ## ## 5) Additional GUI INITIALIZATION (typically with one or two of ## the procs), if needed. ## ## ## Some detail about the code structure of this particular script: ## ## 1a) Define ALL frames: ## ## Top-level : '.fRbuttons' ## '.fRrhs' ## '.fRparms' ## '.fRinit' ## '.fRtime' ## '.fRmsg' ## '.fRcanvas' ## No sub-frames. ## ## 1b) Pack ALL frames. ## ## 2) Define all widgets in the frames (and pack them): ## ## - In '.fRbuttons': ## 5 button widgets ('Exit','Help','Solve','Show','Reset') ## 1 label and 2 radiobuttons ('Animate', 'Start', 'Stop') ## and ## 2 buttons (for setting 2 colors). ## ## - In '.fRrhs': ## 1 label widget (may add an entry widget someday) ## ## - In '.fRparms': ## 3 pairs of 'label' and 'entry' widgets ## ## - In '.fRinit': ## 2 pairs of 'label' and 'entry' widgets ## ## - In '.fRtime': ## 2 paris of 'label' and 'entry' widgets ## ## - In '.fRmsg': ## 1 label widget to display messages to the user, such as ## elapsed execution time, as well as a 'calculation in progress' ## msg or other msgs. ## ## - In '.fRcanvas': 1 'canvas' widget ## ## 3) Define BINDINGS: see the BINDINGS section for bindings, if any ## ## 4) Define procs: ## ## - 'solve' - called by the 'Solve' button ## ## - 'runge-kutta-4' - called by the 'solve' proc ## ## - 'deriv' - called by the 'runge-kutta-4' proc ## ## - 'show_list' - called by the 'ShowList' button ## ## - 'animate' - called by a click on the 'Start' animation radiobutton ## ## - 'setMappingVars_for_px2wc' - called by proc 'animate' ## ## - 'Xpx2wc' - called by proc 'animate' ## - 'Ypx2wc' - called by proc 'animate' ## ## - 'set_pendulum_color1' - called by the 'PendulumColor' button ## ## - 'set_background_color2' - called by the 'BackgroundColor' button ## ## - 'update_color_button' - sets background & foreground color of ## either of the 2 color buttons ## ## - 'advise_user' - called by the 'animate' proc ## ## - 'reset_parms' - called by the 'ResetParms' button ## ## - 'edit_inputs' - called by the'solve' and 'animate' procs ## ## - 'decimal_check' - called by the 'edit_inputs' proc ## ## - 'popup_msgVarWithScroll' - called by the 'Help' button ## ## 5) Additional GUI initialization: ## Set some inital values of parameters ## such as the 2 colors and use a call to ## proc 'reset_parms' to initialize ## angular displacement and angular velocity ## and end-time and time step-size. ## ##+######################################################################## ## DEVELOPED WITH: ## Tcl-Tk 8.5 on Ubuntu 9.10 (2009-october release, 'Karmic Koala'). ## ## $ wish ## % puts "$tcl_version $tk_version" ## showed 8.5 8.5 on Ubuntu 9.10 ## after Tcl-Tk 8.4 was replaced by 8.5 --- to get anti-aliased fonts. ##+####################################################################### ## MAINTENANCE HISTORY: ## Created by: Blaise Montandon 2016jul04 ## Changed by: Blaise Montandon 2016jul17 Added some key bindings to the ## 'popup_msgVarWithScroll' proc. ##+####################################################################### ##+####################################################################### ## Set general window parms (win-title,win-position). ##+####################################################################### wm title . "Simulate Single Pendulum - No Friction - an animation" wm iconname . "Pendulum" wm geometry . +15+30 ##+###################################################### ## Set the color scheme for the window and set the ## background color for the 'trough' in some widgets. ##+###################################################### tk_setPalette "#e0e0e0" set entryBKGD "#f0f0f0" set radbuttBKGD "#f0f0f0" # set scaleBKGD "#f0f0f0" # set chkbuttBKGD "#f0f0f0" # set listboxBKGD "#f0f0f0" ##+######################################################## ## Use a VARIABLE-WIDTH FONT for label and button widgets. ## ## Use a FIXED-WIDTH FONT for listboxes (and ## entry fields, if any). ##+######################################################## font create fontTEMP_varwidth \ -family {comic sans ms} \ -size -14 \ -weight bold \ -slant roman font create fontTEMP_SMALL_varwidth \ -family {comic sans ms} \ -size -12 \ -weight bold \ -slant roman ## Some other possible (similar) variable width fonts: ## Arial ## Bitstream Vera Sans ## DejaVu Sans ## Droid Sans ## FreeSans ## Liberation Sans ## Nimbus Sans L ## Trebuchet MS ## Verdana font create fontTEMP_fixedwidth \ -family {liberation mono} \ -size -14 \ -weight bold \ -slant roman font create fontTEMP_SMALL_fixedwidth \ -family {liberation mono} \ -size -12 \ -weight bold \ -slant roman ## Some other possible fixed width fonts (esp. on Linux): ## Andale Mono ## Bitstream Vera Sans Mono ## Courier 10 Pitch ## DejaVu Sans Mono ## Droid Sans Mono ## FreeMono ## Nimbus Mono L ## TlwgMono ##+########################################################### ## SET GEOM VARS FOR THE VARIOUS WIDGET DEFINITIONS. ## (e.g. width and height of canvas, and padding for Buttons) ##+########################################################### ## BUTTON geom parameters: set PADXpx_button 0 set PADYpx_button 0 set BDwidthPx_button 2 set RELIEF_button "raised" ## LABEL geom parameters: set PADXpx_label 0 set PADYpx_label 0 set BDwidthPx_label 2 # set RELIEF_label "ridge" # set RELIEF_label "raised" set RELIEF_label "flat" ## RADIOBUTTON widget geom settings: set BDwidthPx_radbutt 2 # set RELIEF_radbutt "ridge" set RELIEF_radbutt "raised" ## ENTRY widget geom settings: set BDwidthPx_entry 2 set ParmEntryWidthChars 7 ## SCALE geom parameters: ## (de-activated, for now) if {0} { set BDwidthPx_scale 2 # set initScaleLengthPx 100 set scaleThicknessPx 10 set scaleRepeatDelayMillisecs 800 } ## CANVAS geom parameters: set initCanWidthPx 300 set initCanHeightPx 300 set minCanHeightPx 24 # set BDwidthPx_canvas 2 set BDwidthPx_canvas 0 ##+################################################################### ## Set a MINSIZE of the window. ## ## For width, allow for the minwidth of the '.fRbuttons' frame: ## about 6 buttons, 1 label, 2 radiobuttons ## (Exit,Help,Solve,Reset,Animate,Start,Stop,Color,Color). ## We want to at least be able to see the Exit button. ## ## For height, allow ## 2 chars high for the '.fRbuttons' frame, ## 1 char high for the '.fRrhs' frame, ## 1 char high for the '.fRparms' frame, ## 1 char high for the '.fRinit' frame, ## 1 char high for the '.fRtime' frame, ## 2 chars high for the '.fRmsg' frame, ## 24 pixels high for the '.fRcanvas' frame. ##+################################################################### ## MIN WIDTH: set minWinWidthPx [font measure fontTEMP_varwidth \ "Exit Help Solve Show Reset Animate Start Stop Pendulum Background"] ## Add some pixels to account for right-left-side window decoration ## (about 8 pixels), about 9 x 4 pixels/widget for borders/padding for ## 9 widgets. set minWinWidthPx [expr {44 + $minWinWidthPx}] ## MIN HEIGHT --- allow ## 2 chars high for 'fRbuttons' ## 1 char high for 'fRrhs' ## 1 char high for 'fRparms' ## 1 char high for 'fRinit' ## 1 char high for 'fRtime' ## 2 chars high for 'fRmsg' ## 24 pixels high for 'fRcanvas' set CharHeightPx [font metrics fontTEMP_varwidth -linespace] set minWinHeightPx [expr {24 + (8 * $CharHeightPx)}] ## Add about 28 pixels for top-bottom window decoration, ## about 7x4 pixels for each of the 7 stacked frames and their ## widgets (their borders/padding). set minWinHeightPx [expr {$minWinHeightPx + 56}] ## FOR TESTING: # puts "minWinWidthPx = $minWinWidthPx" # puts "minWinHeightPx = $minWinHeightPx" wm minsize . $minWinWidthPx $minWinHeightPx ## We allow the window to be resizable and we pack the canvas with ## '-fill both -expand 1' so that the canvas can be enlarged by enlarging ## the window. ## If you want to make the window un-resizable, ## you can use the following statement. # wm resizable . 0 0 ##+#################################################################### ## Set a TEXT-ARRAY to hold text for buttons & labels on the GUI. ## NOTE: This can aid INTERNATIONALIZATION. This array can ## be set according to a nation/region parameter. ##+#################################################################### ## if { "$VARlocale" == "en"} ## For widgets in 'fRbuttons' frame: set aRtext(buttonEXIT) "Exit" set aRtext(buttonHELP) "Help" set aRtext(buttonSOLVE) "Solve" set aRtext(buttonSHOW) "ShowList" set aRtext(buttonRESET) "ResetParms" set aRtext(labelANIMATE) "Animate:" set aRtext(radbuttSTART) "Start" set aRtext(radbuttSTOP) "Stop" set aRtext(buttonCOLOR1) "Pendulum Color" set aRtext(buttonCOLOR2) "Background Color" ## For widgets in 'fRrhs' frame: set aRtext(labelRHS) \ "Angular-acceleration expression to be integrated: (g / L) * sin(theta) \ See 'Help'." ## For widgets in 'fRparms' frame: set aRtext(labelG) "g (gravitational acceleration):" set aRtext(labelL) " Length of pendulum arm:" set aRtext(labelIMGSIZE) " Image square (pixels):" ## For widgets in 'fRinit' frame: set aRtext(labelINIT1) "Initial Angular Displacement (degrees):" set aRtext(labelINIT2) " Initial Angular Velocity (degrees/sec):" ## For widgets in 'fRtime' frame: set aRtext(labelENDTIME) "Solve End Time (secs):" set aRtext(labelSTEPSIZE) " Solve Step Size (secs):" set aRtext(labelWAITTIME) " AnimationSteps TimeControl (millisecs):" ## For some calls to the 'advise_user' proc: set aRtext(SOLVEmsg) "*** Click 'Solve' when ready to do the solve ***" set aRtext(STARTmsg) "*** Click 'Start' when ready to start the animation ***" ## END OF if { "$VARlocale" == "en"} ##+################################################################ ## DEFINE *ALL* THE FRAMES: ## ## Top-level : '.fRbuttons' '.fRwave1' '.fRwave2' ## '.fRdistance' '.fRimgsize' '.fRmsg' '.fRcanvas' ##+################################################################ ## FOR TESTING change 0 to 1: ## (Example1: To see appearance of frames when borders are drawn.) ## (Example2: To see sizes of frames for various '-fill' options.) ## (Example3: To see how frames expand as window is resized.) if {0} { set RELIEF_frame raised set BDwidthPx_frame 2 } else { set RELIEF_frame flat set BDwidthPx_frame 0 } frame .fRbuttons -relief $RELIEF_frame -borderwidth $BDwidthPx_frame # frame .fRrhs -relief $RELIEF_frame -borderwidth $BDwidthPx_frame frame .fRrhs -relief raised -borderwidth 2 frame .fRparms -relief $RELIEF_frame -borderwidth $BDwidthPx_frame frame .fRinit -relief $RELIEF_frame -borderwidth $BDwidthPx_frame frame .fRtime -relief $RELIEF_frame -borderwidth $BDwidthPx_frame frame .fRmsg -relief raised -borderwidth 2 # frame .fRcanvas -relief $RELIEF_frame -borderwidth $BDwidthPx_frame frame .fRcanvas -relief raised -borderwidth 2 ##+############################## ## PACK the top-level FRAMES. ##+############################## pack .fRbuttons \ .fRrhs \ .fRparms \ .fRinit \ .fRtime \ .fRmsg \ -side top \ -anchor nw \ -fill x \ -expand 0 pack .fRcanvas \ -side top \ -anchor nw \ -fill both \ -expand 1 ##+######################################################### ## OK. Now we are ready to define the widgets in the frames. ##+######################################################### ##+##################################################################### ## In the '.fRbuttons' FRAME --- DEFINE ## - 'Exit','Help','Solve','ShowList','ResetParms' buttons ## - a label and 2 start/stop radiobuttons ## and ## - 2 buttons ( to specify colors). ## Then PACK all these widgets. ##+##################################################################### button .fRbuttons.buttEXIT \ -text "$aRtext(buttonEXIT)" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command {exit} button .fRbuttons.buttHELP \ -text "$aRtext(buttonHELP)" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command {popup_msgVarWithScroll .topHelp "$HELPtext" +10+10} button .fRbuttons.buttSOLVE \ -text "$aRtext(buttonSOLVE)" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command {solve} button .fRbuttons.buttSHOW \ -text "$aRtext(buttonSHOW)" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -state disabled \ -command {show_list} button .fRbuttons.buttRESET \ -text "$aRtext(buttonRESET)" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command {reset_parms} label .fRbuttons.labelANIMATE \ -text "$aRtext(labelANIMATE)" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## 'VARanimate0or1' is the var for these 2 radiobuttons. set VARanimate0or1 0 radiobutton .fRbuttons.radbuttSTART \ -text "$aRtext(radbuttSTART)" \ -font fontTEMP_varwidth \ -anchor w \ -variable VARanimate0or1 \ -value 1 \ -selectcolor "$radbuttBKGD" \ -relief $RELIEF_radbutt \ -state disabled \ -bd $BDwidthPx_radbutt radiobutton .fRbuttons.radbuttSTOP \ -text "$aRtext(radbuttSTOP)" \ -font fontTEMP_varwidth \ -anchor w \ -variable VARanimate0or1 \ -value 0 \ -selectcolor "$radbuttBKGD" \ -relief $RELIEF_radbutt \ -bd $BDwidthPx_radbutt button .fRbuttons.buttCOLOR1 \ -text "$aRtext(buttonCOLOR1)" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command "set_pendulum_color1" button .fRbuttons.buttCOLOR2 \ -text "$aRtext(buttonCOLOR2)" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command "set_background_color2" ##+########################################### ## Pack the widgets in the 'fRbuttons' frame. ##+########################################### pack .fRbuttons.buttEXIT \ .fRbuttons.buttHELP \ .fRbuttons.buttSOLVE \ .fRbuttons.buttSHOW \ .fRbuttons.buttRESET \ .fRbuttons.labelANIMATE \ .fRbuttons.radbuttSTART \ .fRbuttons.radbuttSTOP \ .fRbuttons.buttCOLOR1 \ .fRbuttons.buttCOLOR2 \ -side left \ -anchor w \ -fill none \ -expand 0 ##+################################################################## ## In the '.fRrhs' FRAME ---- DEFINE 1 LABEL widget. ## Then PACK all these widgets. ##+################################################################## label .fRrhs.labelRHS \ -text "$aRtext(labelRHS)" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bg "#66ff66" \ -bd $BDwidthPx_label pack .fRrhs.labelRHS \ -side left \ -anchor w \ -fill x \ -expand 1 ##+################################################################## ## In the '.fRparms' FRAME ---- DEFINE 3 pairs of ## LABEL-and-ENTRY widgets. ## Then PACK all these widgets. ##+################################################################## label .fRparms.labelG \ -text "$aRtext(labelG)" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYg "9.8" entry .fRparms.entryG \ -textvariable ENTRYg \ -bg $entryBKGD \ -font fontTEMP_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ## FOR PENDULUM LENGTH: label .fRparms.labelL \ -text "$aRtext(labelL)" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYlength "10.0" entry .fRparms.entryL \ -textvariable ENTRYlength \ -bg $entryBKGD \ -font fontTEMP_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ## FOR IMAGE SIZE: label .fRparms.labelIMGSIZE \ -text "$aRtext(labelIMGSIZE)" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYimgsize "300" entry .fRparms.entryIMGSIZE \ -textvariable ENTRYimgsize \ -bg $entryBKGD \ -font fontTEMP_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ######################################## ## PACK the widgets in frame '.fRparms'. ######################################## pack .fRparms.labelG \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRparms.entryG \ -side left \ -anchor w \ -fill x \ -expand 0 pack .fRparms.labelL \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRparms.entryL \ -side left \ -anchor w \ -fill x \ -expand 0 pack .fRparms.labelIMGSIZE \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRparms.entryIMGSIZE \ -side left \ -anchor w \ -fill x \ -expand 0 ##+################################################################## ## In the '.fRinit' FRAME ---- DEFINE 2 pairs of ## LABEL and ENTRY widgets. ## Then PACK all these widgets. ##+################################################################## label .fRinit.labelINIT1 \ -text "$aRtext(labelINIT1)" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYinit1 "8" entry .fRinit.entryINIT1 \ -textvariable ENTRYinit1 \ -bg $entryBKGD \ -font fontTEMP_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ## FOR INITIAL VELOCITY: label .fRinit.labelINIT2 \ -text "$aRtext(labelINIT2)" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYinit2 "0.0" entry .fRinit.entryINIT2 \ -textvariable ENTRYinit2 \ -bg $entryBKGD \ -font fontTEMP_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ######################################## ## PACK the widgets in frame '.fRinit'. ######################################## pack .fRinit.labelINIT1 \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRinit.entryINIT1 \ -side left \ -anchor w \ -fill x \ -expand 0 pack .fRinit.labelINIT2 \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRinit.entryINIT2 \ -side left \ -anchor w \ -fill x \ -expand 0 ##+################################################################## ## In the '.fRtime' FRAME ---- DEFINE 2 pairs of ## LABEL and ENTRY widgets. ## Then PACK all these widgets. ##+################################################################### label .fRtime.labelENDTIME \ -text "$aRtext(labelENDTIME)" \ -font fontTEMP_SMALL_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYendtime "20.0" entry .fRtime.entryENDTIME \ -textvariable ENTRYendtime \ -bg $entryBKGD \ -font fontTEMP_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ## FOR INTEGRATION (solver) STEPSIZE: label .fRtime.labelSTEPSIZE \ -text "$aRtext(labelSTEPSIZE)" \ -font fontTEMP_SMALL_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYstepsize "0.0" entry .fRtime.entrySTEPSIZE \ -textvariable ENTRYstepsize \ -bg $entryBKGD \ -font fontTEMP_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ## FOR ANIMATION-STEPS WAIT-TIME: ## (DE-ACTIVATED FOR NOW) if {0} { label .fRtime.labelWAITTIME \ -text "$aRtext(labelWAITTIME)" \ -font fontTEMP_SMALL_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYmillisecs "0.0" entry .fRtime.entryWAITTIME \ -textvariable ENTRYmillisecs \ -bg $entryBKGD \ -font fontTEMP_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry } ## END OF if {0} ######################################## ## PACK the widgets in frame '.fRtime'. ######################################## pack .fRtime.labelENDTIME \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRtime.entryENDTIME \ -side left \ -anchor w \ -fill x \ -expand 0 pack .fRtime.labelSTEPSIZE \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRtime.entrySTEPSIZE \ -side left \ -anchor w \ -fill x \ -expand 0 ## We do not implement the wait-time entry field, for now. if {0} { pack .fRtime.labelWAITTIME \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRtime.entryWAITTIME \ -side left \ -anchor w \ -fill x \ -expand 0 } ## END OF if {0} ##+################################################################## ## In the '.fRmsg' FRAME ---- DEFINE-and-PACK 1 LABEL widget. ##+################################################################## label .fRmsg.labelINFO \ -text "" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief flat \ -bg "#ff6666" \ -bd $BDwidthPx_button pack .fRmsg.labelINFO \ -side left \ -anchor w \ -fill x \ -expand 1 ##+###################################################### ## In the '.fRcanvas' FRAME - DEFINE the 'canvas' widget ## --- no scrollbars, for now. ## Then PACK the widget(s). ##+###################################################### ## We set highlightthickness & borderwidth of the canvas to ## zero, as suggested on page 558, Chapter 37, 'The Canvas ## Widget', in the 4th edition of the book 'Practical ## Programming in Tcl and Tk'. ##+###################################################### canvas .fRcanvas.can \ -width $initCanWidthPx \ -height $initCanHeightPx \ -relief flat \ -highlightthickness 0 \ -borderwidth 0 # -yscrollcommand ".fRcanvas.scrolly set" \ # -xscrollcommand ".fRcanvas.scrollx set" # scrollbar .fRcanvas.scrolly \ # -orient vertical \ # -command ".fRcanvas.can yview" # scrollbar .fRcanvas.scrollx \ # -orient horizontal \ # -command ".fRcanvas.can xview" ##+####################################################### ## PACK the widgets in frame '.fRcanvas'. ## (Skip the scrollbar pack statements, for now.) ##+####################################################### if {0} { ########################################################### ## NOTE: ## NEED TO PACK THE SCROLLBARS BEFORE THE CANVAS WIDGET. ## OTHERWISE THE CANVAS WIDGET TAKES ALL THE FRAME SPACE. ##+####################################################### pack .fRcanvas.scrolly \ -side right \ -anchor e \ -fill y \ -expand 0 pack .fRcanvas.scrollx \ -side bottom \ -anchor s \ -fill x \ -expand 0 ################################################################# ## !!!NEED TO USE '-expand 0' FOR THE X AND Y SCROLLBARS, so that ## the canvas is allowed to fill the remaining frame-space nicely ## --- without a gap between the canvas and its scrollbars. ################################################################# } ## END OF if {0} pack .fRcanvas.can \ -side top \ -anchor n \ -fill none \ -expand 0 ## We don't let the canvas expand, because we want ## to make it a square with colored background --- ## centered at the bottom of the GUI. # -fill both \ # -expand 1 ##+######################################## ## END OF the DEFINITION OF THE GUI WIDGETS ##+######################################## ##+############################### ## BINDINGS SECTION: ##+############################### ##+################################################################### ## Remind user to click 'Solve' after changing a math expression ## parameter or an initial condition or a time parameter. ##+################################################################### bind .fRparms.entryG \ {advise_user "$aRtext(SOLVEmsg) with new 'g' value."} bind .fRparms.entryL \ {advise_user "$aRtext(SOLVEmsg) with new 'L' value."} bind .fRinit.entryINIT1 \ {advise_user "$aRtext(SOLVEmsg) with new initial angle value."} bind .fRinit.entryINIT2 \ {advise_user "$aRtext(SOLVEmsg) with new initial angular velocity value."} bind .fRtime.entryENDTIME \ {advise_user "$aRtext(SOLVEmsg) with new END-TIME."} bind .fRtime.entrySTEPSIZE \ {advise_user "$aRtext(SOLVEmsg) with new STEP-SIZE."} ##+################################################################ ## Remind user to click animate 'Start' button after changing ## an animation-related parameter. ##+################################################################ bind .fRparms.entryIMGSIZE \ {advise_user "$aRtext(STARTmsg) with new image size value."} bind .fRbuttons.buttCOLOR1 \ {advise_user "$aRtext(STARTmsg) with new pendulum color."} bind .fRbuttons.buttCOLOR2 \ {advise_user "$aRtext(STARTmsg) with new background color."} ##+####################################################### ## Start the animation with a button1-release on the ## 'Start' radiobutton. ##+####################################################### bind .fRbuttons.radbuttSTART {animate} ##+###################################################################### ## PROCS SECTION: ## ## - 'solve' - called by the 'Solve' button. ## ## - 'runge-kutta-4' - called by the 'solve' proc. ## ## - 'deriv' - called by the 'runge-kutta-4' proc. ## ## - 'show_list' - called by the 'ShowList' button ## ## - 'animate' - called by a click on the 'Start' animation radiobutton. ## ## - 'setMappingVars_for_px2wc' - called by proc 'animate'. ## ## - 'Xpx2wc' - called by proc 'animate'. ## - 'Ypx2wc' - called by proc 'animate'. ## ## - 'set_pendulum_color1' - called by the 'PendulumColor' button. ## ## - 'set_background_color2' - called by the 'BackgroundColor' button. ## ## - 'update_color_button' - sets background & foreground color of ## either of the 2 color buttons. ## ## - 'advise_user' - called by the 'animate' proc. ## ## - 'reset_parms' - called by the 'ResetParms' button and in the ## 'Additional GUI Initialization' section, ## to initialize the parms. ## ## - 'edit_inputs' - called by 'solve' and 'animate' procs ## ## - 'decimal_check' - called by 'edit_inputs' proc ## ## - 'popup_msgVarWithScroll' - called by the 'Help' button. ## ##+####################################################################### ##+##################################################################### ## PROC solve ## ## PURPOSE: ## Peforms the integration of the pair of first order equations: ## D(u1) = u2 ## D(u2) = (g/L) * sin(u1) ## ## METHOD: ## First do some intialization of variables. Then, in a loop, ## we use the 'runge-kutta-4' proc to get the output u1,u2 for each ## time step. The 'deriv' proc provides the derivative ## (rate of change) evaluations needed for the Runge-Kutta-order-4 ## numerical integration process. ## ## The output is saved in three global arrays aRtime, aRu1, aRu2 ## which are indexed by integers representing the time-step number. ## ## aRu1 and aRu2 are saved in radians rather than degrees. ## ## CALLED BY: By clicking on the 'Solve' button. ##+#################################################################### proc solve {} { ## FOR TESTING: (dummy out this proc) # return global ENTRYg ENTRYlength ENTRYinit1 ENTRYinit2 \ ENTRYendtime ENTRYstepsize EDITcode \ pi twopi radsPERdeg \ aRtime aRu1 aRu2 Nsteps ######################################################## ## Check the entry field inputs. ######################################################## edit_inputs if {$EDITcode > 0} {return} ################################################ ## Set the current time, for determining elapsed ## time for building each 'photo' image. ################################################ set t0 [clock milliseconds] ##################################################################### ## We use local variables time, u1, and u2 to hold the current ## values of the independent variable and 2 dependent variables. ## We use radians as the units for u1 and u2. ## ## We initialize those 3 local variables here --- by converting ## ENTRYinit1 and ENTRYinit2 from degrees to radians. ##################################################################### set time 0.0 set u1 [expr {$radsPERdeg * $ENTRYinit1}] set u2 [expr {$radsPERdeg * $ENTRYinit2}] #################################################################### ## Initialize the 3 arrays in which we store the solution results. #################################################################### set Nsteps 0 set aRtime($Nsteps) 0.0 set aRu1($Nsteps) $u1 set aRu2($Nsteps) $u2 #################################################################### ## Start the solution loop. #################################################################### set ENDtime [expr {double($ENTRYendtime)}] set h [expr {double($ENTRYstepsize)}] while {$time <= $ENDtime} { incr Nsteps set LISTu1outANDu2out [runge-kutta-4 $time $h $u1 $u2] set aRu1($Nsteps) [lindex $LISTu1outANDu2out 0] set aRu2($Nsteps) [lindex $LISTu1outANDu2out 1] ##################################################### ## Set the 'time' variable for the next step, if any. ## Also save that time as the time location of the ## two outputs aRu1($Nsteps) and aRu2($Nsteps). ##################################################### set time [expr {$time + $h}] set aRtime($Nsteps) $time ######################################################## ## Set the u1 and u2 variables for the next step, if any. ######################################################## set u1 $aRu1($Nsteps) set u2 $aRu2($Nsteps) } ## END OF solution loop ######################################################## ## Show the user the elapsed-time for this solution run. ######################################################## set solvetime [expr {[clock milliseconds] - $t0}] set simtime [format "%.4f" $time] advise_user "\ ** SOLVE DONE: $solvetime millisecs elapsed ; SIMULATED TIME: $simtime secs ; Steps: $Nsteps **" ######################################################## ## Activate the initially disabled 'ShowList' button ## and the 'Start' radiobutton. ######################################################## .fRbuttons.buttSHOW configure -state normal .fRbuttons.radbuttSTART configure -state normal } ## END OF PROC 'solve' ##+##################################################################### ## PROC show_list ## ## PURPOSE: ## Creates a columnar list of results of the form: ## t u1 u2 ## where ## t is the time in secs, from 0.0 to ENTRYendtime ## u1 the angular displacement, in radians ## u2 the angular velocity, in radian/sec ## ## Also adds 2 columns corresponding to u1 and u2 ## in degrees, instead of radians. ## ## Puts the list in a text variable, VARlist, and ## shows the list in a popup window by using the ## 'popup_msgVarWithScroll' proc. ## ## METHOD: Uses the 3 arrays created by the 'solve' proc. ## ## CALLED BY: By clicking on the 'ShowList' button. ##+#################################################################### proc show_list {} { ## FOR TESTING: (dummy out this proc) # return global ENTRYg ENTRYlength ENTRYinit1 ENTRYinit2 \ ENTRYendtime ENTRYstepsize \ pi twopi radsPERdeg \ aRtime aRu1 aRu2 Nsteps ################################## ## Create the heading for the list. ################################## set VARlist \ "Simulation of a Single Pendulum, No Friction g (gravitational acceleration) = $ENTRYg L (length of pendulum arm) = $ENTRYlength Initial angular displacement from vertical (degrees): $ENTRYinit1 Initial angular displacement from vertical (radians): [expr {$ENTRYinit1 * $radsPERdeg}] Initial angular velocity (degrees/sec): $ENTRYinit2 Initial angular velocity (radians/sec): [expr {$ENTRYinit2 * $radsPERdeg}] Start time (secs): 0.0 End time (secs): $ENTRYendtime Solver time step (secs): $ENTRYstepsize Number of time steps: $Nsteps Time Angular Displacement Angular Velocity Angular Displacement Angular Velocity (secs) (radians) (radians/sec) (degrees) (degrees/sec) ---------- -------------------- -------------------- -------------------- -------------------- " ######################################################## ## In a loop over the integer-index of the 3 'aR' arrays, ## put the 3 numbers in each row of the list. ######################################################## set idx 0 while {$idx <= $Nsteps} { set timeFMTED [format "%10.3f" $aRtime($idx)] set u1FMTED [format "%20.3f" $aRu1($idx)] set u2FMTED [format "%20.3f" $aRu2($idx)] set u1deg [expr {$aRu1($idx) / $radsPERdeg}] set u2deg [expr {$aRu2($idx) / $radsPERdeg}] set u1degFMTED [format "%20.3f" $u1deg] set u2degFMTED [format "%20.3f" $u2deg] append VARlist "$timeFMTED $u1FMTED $u2FMTED $u1degFMTED $u2degFMTED \n" incr idx } ## END OF while loop ####################################### ## Add usage info to bottom of the list. ####################################### append VARlist " --- With a mouse, you can highlight the lines above and paste them into a text editor window and save that text into a file. Then you can use that text file in a plot utility, such as 'gnuplot', to plot the angular-displacement and/or the angular-velocity against time. " ############################## ## Show the list. ############################## popup_msgVarWithScroll .topList "$VARlist" +20+30 } ## END OF PROC 'show_list' ##+############################################################## ## PROC 'runge-kutta-4' ## ## PURPOSE: Let u(t) be a vector function of independent variable ## t (a scalar) --- in this case a 2-dimensional vector ## function composed of 2 scalar functions u1(t) and u2(t). ## ## Let f(t,u) be a vector function of t and vector u --- ## in this case a 2-dimensional vector function ## composed of 2 functions f1(t,u1,u2) and f2(t,u1,u2). ## ## For a given value of t and h (a step-size of t), ## this proc is TO RETURN a pair of floating-point values ## corresponding to using a Runge-Kutta 4th order ## method to evaluate u1(t+h) and u2(t+h). ## ##+###### ## METHOD: ## ## For a first-order differential equation of the form ## ## D(u) = f(t,u) where D is the d/dt derivative operator ## ## and where t is the independent variable and u is a vector ## function of t, ## ## the Runge-Kutta 4th order method (RK4) is often written in the form ## ## u(t+h) = u(t) + (Ka + 2 * Kb + 2 * Kc + Kd) / 6 ## where ## Ka = h * f (t , u(t) ) ## Kb = h * f (t + (h/2), u(t) + (Ka / 2) ) ## Kc = h * f (t + (h/2), u(t) + (Kb / 2) ) ## Kd = h * f (t + h, u(t) + Kc ) ## where ## f and u and the K's are vectors (2-dimensional in this ## oscillating single pendulum case). ## ##+########################################### ## An alternate way of writing these equations: ## (essentially factoring out the factor 'h' from the K eqns) ## ## u(t+h) = u(t) + (h/6) * (Da + 2*Db + 2*Dc + Dd) ## where ## Da = f (t , u(t) ) ## Db = f (t + (h/2), u(t) + (h/2)*Da ) ## Dc = f (t + (h/2), u(t) + (h/2)*Db ) ## Dd = f (t + h, u(t) + h*Dc ) ## ##+############### ## Scalar eqns instead of vector eqns: ## ## Let us rewrite these vector equations as twice as many ## scalar equations involving scalar functions --- u1,u2,f1,f2, ## Da1,Da2,Db1,Db2,Dc1,Dc2,Dd1,Dd2. ## ## Let us rewrite them in an order in which they would need ## to be computed. ## ## Da1 = f1 (t , u1(t), u2(t) ) ## Da2 = f2 (t , u1(t), u2(t) ) ## ## Db1 = f1 (t + (h/2), u1(t) + (h/2)*Da1, u2(t) + (h/2)*Da2 ) ## Db2 = f2 (t + (h/2), u1(t) + (h/2)*Da1, u2(t) + (h/2)*Da2 ) ## ## Dc1 = f1 (t + (h/2), u1(t) + (h/2)*Db1, u2(t) + (h/2)*Db2 ) ## Dc2 = f2 (t + (h/2), u1(t) + (h/2)*Db1, u2(t) + (h/2)*Db2 ) ## ## Dd1 = f1 (t + h, u1(t) + h*Dc1, u2(t) + h*Dc2) ## Dd2 = f2 (t + h, u1(t) + h*Dc1, u2(t) + h*Dc2) ## ## u1(t+h) = u1(t) + (h/6) * (Da1 + 2*Db1 + 2*Dc1 + Dd1) ## u2(t+h) = u2(t) + (h/6) * (Da2 + 2*Db2 + 2*Dc2 + Dd2) ## ## This proc returns u1(t+h) and u2(t+h) for given t and h. ## ## The values of f1 and f2 for given u1 and u2 --- and t --- ## are given by calls to proc 'deriv' --- before each pair ## of D equations. ## ## CALLED BY: the 'solve' proc. ##+############################################################## proc runge-kutta-4 {t h u1 u2} { ## FOR TESTING: (dummy out this proc) # return ###################################################### ## Use the 'deriv' proc to get deriv values f1 and f2 ## for the given t,u1,u2. ###################################################### set LISTf1ANDf2 [deriv $t $u1 $u2] set Da1 [lindex $LISTf1ANDf2 0] set Da2 [lindex $LISTf1ANDf2 1] set h2 [expr { $h / 2.0 }] set t_h2 [expr { $t + $h2 }] set u1_hD [expr {$u1 + ($h2 * $Da1)}] set u2_hD [expr {$u2 + ($h2 * $Da2)}] set LISTf1ANDf2 [deriv $t_h2 $u1_hD $u2_hD] set Db1 [lindex $LISTf1ANDf2 0] set Db2 [lindex $LISTf1ANDf2 1] set u1_hD [expr {$u1 + ($h2 * $Db1)}] set u2_hD [expr {$u2 + ($h2 * $Db2)}] set LISTf1ANDf2 [deriv $t_h2 $u1_hD $u2_hD] set Dc1 [lindex $LISTf1ANDf2 0] set Dc2 [lindex $LISTf1ANDf2 1] set t_h [expr { $t + $h}] set u1_hD [expr {$u1 + ($h * $Dc1)}] set u2_hD [expr {$u2 + ($h * $Dc2)}] set LISTf1ANDf2 [deriv $t_h $u1_hD $u2_hD] set Dd1 [lindex $LISTf1ANDf2 0] set Dd2 [lindex $LISTf1ANDf2 1] set h6 [expr { $h / 6.0 }] set u1out [expr {$u1 + ( $h6 * ($Da1 + (2.0 * ($Db1 + $Dc1)) + $Dd1) )}] set u2out [expr {$u2 + ( $h6 * ($Da2 + (2.0 * ($Db2 + $Dc2)) + $Dd2) )}] return "$u1out $u2out" } ## END OF PROC 'runge-kutta-4' ##+############################################################## ## PROC 'deriv' ## ## PURPOSE: For a given independent variable, t, and for a value ## of a vector u (in this case, a 2-dimensional vector ## with values u1 and u2), this proc is ## TO RETURN a pair of floating-point values for a ## vector function f(t,u) (in this case, for a ## 2-dimensional vector f). ## ## In other words, for 2 scalar functions ## f1(t,u1,u2) ## f2(t,u1,u2) ## this proc returns two floating point values ## determined by evaluating f1 and f2 for the ## given floating point values t, u1, and u2. ## ## METHOD: In this application, ## f1(t,u1,u2) = u2 ## and ## f2(t,u1,u2) = - (g/L) * sin(u1) ## where u1 is expected to be in radians, not degrees. ## ## CALLED BY: the 'runge-kutta-4' proc. ##+############################################################## proc deriv {t u1 u2} { ## FOR TESTING: (dummy out this proc) # return global ENTRYg ENTRYlength set f1 $u2 set f2 [expr { (-1.0) * ($ENTRYg / $ENTRYlength) * sin($u1) }] ## NOTE: ## t is not used in these functions, but we are ## prepared to allow adding a forcing function ## such as A * sin(k * t) return "$f1 $f2" } ## END OF PROC 'deriv' ##+##################################################################### ## PROC animate ## ## PURPOSE: ## Starts drawing and showing the sequence of pendulum positions ## --- the 'arm' with 'create line' statements and the mass at ## the end of the arm with 'create oval' statements. ## ## METHOD: ## The angle-displacement results in the array 'aRu1' (in radians) ## are used --- along with Tcl sin() and cos() functions and the ## arm length L --- to calculate the coordinates of the end of ## the pendulum arm. ## ## Periodically checking the status of the 'Stop' radiobutton ## allows the user to stop the animation before the last ## time step is reached. ## ## CALLED BY: By clicking on the 'Start' animation radiobutton. ##+##################################################################### proc animate {} { ## FOR TESTING: (dummy out this proc) # return global VARanimate0or1 aRu1 Nsteps ENTRYlength \ ENTRYimgsize radiusPx ENTRYstepsize EDITcode \ COLOR1r COLOR1g COLOR1b COLOR1hex \ COLOR2r COLOR2g COLOR2b COLOR2hex ############################################################ ## If the Nsteps variable does not exist (no solver run yet), ## do not perform an animation. ############################################################ if {[info exists Nsteps] == 0} {return} ######################################################## ## Check the entry field inputs. ######################################################## edit_inputs if {$EDITcode > 0} {return} ############################################################ ## Set WAITmillisecs variable from ENTRYstepsize seconds. ## (WAITmillisecs is used between each draw of a ## pendulum position.) ############################################################ set WAITmillisecs [expr {int(1000.0 * $ENTRYstepsize)}] ############################################################ ## Clear the message area --- replace with an animation msg. ############################################################ advise_user "* Animation in progress. Click 'Stop' radiobutton to halt. *" ############################################################## ## Reconfigure the canvas into a square image area that ## we are going to use --- from the user-specified image size ## in variable ENTRYimgsize. ## ## We want the canvas background color to delineate a square ## area on which the animated pendulum image will be drawn. ############################################################## .fRcanvas.can configure -width $ENTRYimgsize .fRcanvas.can configure -height $ENTRYimgsize ############################################################ ## Set the background color of the canvas area from the ## current background color variable. ############################################################ .fRcanvas.can config -bg $COLOR2hex ############################################################# ## We use a 'wm geometry' command to get the window (and the ## canvas widget) to resize appropriately --- even after the ## user has manually resized the top window. ## ## Reference: wiki.tcl.tk/10720 and wiki.tcl.tk/44 ## and page 237 of Ousterhout's book 'Tcl and the Tk Toolkit': ## "If you would like to restore a window to its natural ## size, you can invoke 'wm geometry' with an empty ## geometry string." ############################################################# wm geometry . {} ################################################################ ## Set the variables for converting pixels to world-coords. ## This is in case the user changed the image dimensions. ################################################################ ## Recall input vars for proc 'setMappingVars_for_px2wc' are: ## xORy,ULwcX,ULwcY,ULpxX,ULpxY,LRwcX,LRwcY,LRpxX,LRpxY ## ## In order to allow some margin: ## ## We map world-coord X-limits -L and +L (or a little bigger) ## TO pixel-coord X-limits 0 and ENTRYimgsize (left and right) ## ## AND ## ## We map world-coord Y-limits +L and -L (or a little bigger) ## TO pixel-coord Y-limits 0 and ENTRYimgsize (top and bottom) ## ## See code in 'setMappingVars_for_px2wc' for details. ######################################################################## set Lbigger [expr {1.2 * $ENTRYlength}] set plusL $Lbigger set minusL [expr {-$Lbigger}] set XwcUL $minusL set YwcUL $plusL set XwcLR $plusL set YwcLR $minusL setMappingVars_for_px2wc xy $XwcUL $YwcUL 0 0 $XwcLR $YwcLR $ENTRYimgsize $ENTRYimgsize ################################################################### ## Set the coordinates of the fixed pivot-point of the pendulum arm ## in world-coords: (x1,y1). And convert those world-coordinates ## to pixel coordinates, for use in the 'create line' commands below. ################################################################### set x1 0.0 set y1 0.0 set x1Px [Xwc2px $x1] set y1Px [Ywc2px $y1] ############################################################### ## LOOP over the index of the array aRu1 (angular displacement) ## to perform the animation of the pendulum. ############################################################### ## This while loop stops when VARanimate0or1 is set to 0 ## --- by the user clicking on the 'Stop' radiobutton. ## ## At the bottom of each pass through the loop, the results-array ## index is incremented. If that index exceeds Nsteps, ## we 'break' out of this animation loop. ############################################################### set idx 0 while {$VARanimate0or1 == 1} { ####################################################### ## Delete the pendulum arm and mass (line and circle) ## from a previous draw iteration. ####################################################### .fRcanvas.can delete all ################################################################# ## Set the coordinates of the end-point of the pendulum arm ## in world-coords: (x2,y2). And convert those world-coordinates ## to pixel coordinates, for use in the 'create line' command below. ################################################################# set x2 [expr {$ENTRYlength * sin($aRu1($idx))}] set y2 [expr {(-1) * $ENTRYlength * cos($aRu1($idx))}] set x2Px [Xwc2px $x2] set y2Px [Ywc2px $y2] ########################################################################## ## Draw the pendulum arm --- with 'create line'. ########################################################################## .fRcanvas.can create line $x1Px $y1Px $x2Px $y2Px \ -tags TAGarm -fill "$COLOR1hex" ########################################################################## ## Draw the mass at the end of the pendulum arm --- with 'create oval'. ## Set the upper-left and lower-right corners of the oval (a circle ## in this application). ########################################################################## set massX1Px [expr {$x2Px - $radiusPx}] set massY1Px [expr {$y2Px - $radiusPx}] set massX2Px [expr {$x2Px + $radiusPx}] set massY2Px [expr {$y2Px + $radiusPx}] .fRcanvas.can create oval $massX1Px $massY1Px $massX2Px $massY2Px \ -tags TAGmass -fill "$COLOR1hex" ########################################################## ## Make sure the image is shown before going on to build ## the next image (the next pendulum position). ########################################################## update ########################################################## ## Wait a few millisecs before proceeding to erase the ## current pendulum drawing and before preparing to ## to draw the next pendulum position. ## (To get a better wait-time to make the oscillation ## times more accurate, we could get the draw time using ## a couple of calls to 'clock milliseconds' before and ## after each draw and subtract the indicated draw-time ## from WAITmillsecs.) ########################################################## after $WAITmillisecs incr idx ########################################################### ## If we exceed the number of elements in the array aRu1 ## (that is, the last of the time steps), break out of this ## animation loop. ########################################################### if {$idx > $Nsteps} {break} } ## END OF the animation loop (over the index of array aRu1) ############################################################ ## Clear the message area --- replace with an animation msg. ############################################################ advise_user "\ ** ANIMATION ENDED: NumberOfTimeStepsAvailable: $Nsteps **" set VARanimate0or1 0 } ## END OF PROC 'animate' ##+######################################################################## ## PROC 'setMappingVars_for_px2wc' ##+######################################################################## ## PURPOSE: Sets up 'constants' to be used in converting between x,y ## 'world coordinates' and 'pixel coordinates' on a Tk canvas. ## ## Puts the constants in global variables: ## PXperWC BASEwcX BASEwcY BASEpxX BASEpxY ## ## These variables are for use by 'Xwc2px' and 'Ywc2px' procs ## and 'Xpx2wc' and 'Ypx2wc' procs. ## ## The 'BASE' variables are coordinates of the upper-left point ## of the 'plotting rectangle' --- in world coordinates and ## in pixel coordinates. ## ## METHOD: This proc takes the coordinates of an UpperLeft (UL) ## point and a LowerRight (LR) point --- in both ## 'world coordinates' and 'pixel coordinates' and ## sets some global variables to the used by the ## other drawing procs --- mainly the ratio: ## ## the number-of-pixels-per-world-coordinate-unit, ## in global variable 'PXperWC' ## ## (This will generally include a fractional amount, ## i.e. it is not necessarily an integer.) ## ## INPUTS: ## ## At least eight numbers are input to this proc, as indicated by: ## ## ULwcX ULwcY ULpxX ULpxY LRwcX LRwcY LRpxX LRpxY ## ## Generally, the 'wc' inputs may be floating point numbers, and the ## 'px' inputs will generally be (non-negative) integers. ## ## Example: (for a plot area with x between -1.2 and +1.2 ## and with y between -0.2 and +1.2) ## setMappingVars_for_px2wc xy -1.2 1.2 0 0 1.2 -0.2 $canvasWidthPx $canvasHeightPx ## ## The first argument can be either 'x' or 'y' or 'xy'. This determines whether ## global variable 'PXperWC' is detemined by just the X-numbers, just the Y-numbers, ## or both. In this script, we use 'xy' (both). ## ## An 'adjustYpx' global variable can be used to adjust if the pixels ## on a user's monitor are not square. ## ## OUTPUTS: global variables PXperWC BASEwcX BASEwcY BASEpxX BASEpxY ## ## CALLED BY: by the animate' proc. ##+######################################################################## proc setMappingVars_for_px2wc {xORy ULwcX ULwcY ULpxX ULpxY LRwcX LRwcY LRpxX LRpxY} { global PXperWCx PXperWCy BASEwcX BASEwcY BASEpxX BASEpxY adjustYpx ## FOR TESTING: (to dummy out this proc) # return ############################################################ ## Calculate PXperWCx and PXperWCy ## (pixels-per-world-coordinate-unit) --- the ratio ## of pixels-per-world-coordinate in the x and y directions, ## for the given UL and LR values. ############################################################ set PXperWCx [expr {abs(($LRpxX - $ULpxX) / ($LRwcX - $ULwcX))}] set PXperWCy [expr {abs(($LRpxY - $ULpxY) / ($LRwcY - $ULwcY))}] ## FOR TESTING: if {0} { puts "proc 'animate':" puts "LRwcY: $LRwcY ULwcY: $ULwcY LRwcX: $LRwcX ULwcX: $ULwcX" puts "PXperWCx: $PXperWCx" puts "LRpxY: $LRpxY ULpxY: $ULpxY LRpxX: $LRpxX ULpxX: $ULpxX" puts "PXperWCy: $PXperWCy" } ############################################################# ## Reset PXperWCx and PXperWCy according to whether input ## variable 'xORy' is 'x' or 'y' or 'min' or 'xy'. ## ## For 'x', we set PXperWCy equal to PCperWcx. ## ## For 'y', we set PXperWCx equal to PCperWcy. ## ## For 'min', we set PXperWCx and PXperWCy to the smaller ## of PXperWCx and PXperWCy. ## ## For 'xy', we will leave PXperWCx and PXperWCy unchanged. ############################################################# if {$xORy == "x"} { set PXperWCy $PXperWCx } elseif {$xORy == "y"} { set PXperWCx $PXperWCy } elseif {$xORy == "min"} { if {$PXperWCx > $PXperWCy} { set PXperWCx $PXperWCy } else { set PXperWCy $PXperWCx } } ## END OF if {$xORy == "x"} ############################################################ ## In case the pixels are not square, provide a factor ## that can be used to adjust in the Y direction. ############################################################ set adjustYpx 1.0 ############################################################ ## Set BASEwcX, BASEwcY, BASEpxX and BASEpxY. ############################################################ set BASEwcX $ULwcX set BASEwcY $ULwcY set BASEpxX $ULpxX set BASEpxY $ULpxY ## FOR TESTING: if {0} { puts "proc 'setMappingVars_for_px2wc':" puts "PXperWCx: $PXperWCx PXperWCy: $PXperWCy" puts "BASEwcX: $BASEwcX BASEwcY: $BASEwcY" puts "BASEpxX: $BASEpxX BASEpxY: $BASEpxY" } } ## END OF PROC 'setMappingVars_for_px2wc' ##+######################################################################## ## PROC 'Xwc2px' ##+######################################################################## ## PURPOSE: Converts an x world-coordinate to pixel units. ## ## CALLED BY: the 'draw' procs ##+######################################################################## proc Xwc2px {x} { global PXperWCx BASEwcX BASEpxX set px [expr {($x - $BASEwcX) * $PXperWCx + $BASEpxX}] return $px } ## END OF PROC 'Xwc2px' ##+######################################################################## ## PROC 'Xpx2wc' ##+######################################################################## ## PURPOSE: Converts an x-pixel unit to an x-world-coordinate value. ## ## CALLED BY: the 'StartAnimation' proc ##+######################################################################## proc Xpx2wc {px} { global PXperWCx BASEwcX BASEpxX set x [expr {( ($px - $BASEpxX) / $PXperWCx ) + $BASEwcX }] return $x } ## END OF PROC 'Xpx2wc' ##+######################################################################## ## PROC 'Ywc2px' ##+######################################################################## ## PURPOSE: Converts an y world-coordinate to pixel units. ## ## CALLED BY: the 'draw' procs ##+######################################################################## proc Ywc2px {y} { global PXperWCy BASEwcY BASEpxY adjustYpx set px [expr {($BASEwcY - $y) * $PXperWCy * $adjustYpx + $BASEpxY}] return $px } ## END OF PROC 'Ywc2px' ##+######################################################################## ## PROC 'Ypx2wc' ##+######################################################################## ## PURPOSE: Converts a y-pixel unit to a y-world-coordinate value. ## ## CALLED BY: the 'StartAnimation' proc ##+######################################################################## proc Ypx2wc {px} { global PXperWCy BASEwcY BASEpxY adjustYpx set y [expr { $BASEwcY - ( ($px - $BASEpxY) / ( $PXperWCy * $adjustYpx ) ) }] return $y } ## END OF PROC 'Ypx2wc' ##+##################################################################### ## proc 'set_pendulum_color1' ##+##################################################################### ## PURPOSE: ## ## This procedure is invoked to get an RGB triplet ## via 3 RGB slider bars on the FE Color Selector GUI. ## ## Uses that RGB value to set a 'pendulum' color. ## ## Arguments: global variables ## ## CALLED BY: .fRbuttons.buttCOLOR1 button ##+##################################################################### proc set_pendulum_color1 {} { global COLOR1r COLOR1g COLOR1b COLOR1hex ColorSelectorScript aRtext ## FOR TESTING: # puts "COLOR1r: $COLOR1r" # puts "COLOR1g: $COLOR1g" # puts "COLOR1b: $COLOR1b" set TEMPrgb [ exec $ColorSelectorScript $COLOR1r $COLOR1g $COLOR1b] ## FOR TESTING: # puts "TEMPrgb: $TEMPrgb" if { "$TEMPrgb" == "" } { return } scan $TEMPrgb "%s %s %s %s" r255 g255 b255 hexRGB set COLOR1hex "#$hexRGB" set COLOR1r $r255 set COLOR1g $g255 set COLOR1b $b255 ## Set background-and-foreground colors of the indicated color button. update_color_button color1 advise_user "$aRtext(STARTmsg) to use a new color" } ## END OF proc 'set_pendulum_color1' ##+##################################################################### ## proc 'set_background_color2' ##+##################################################################### ## PURPOSE: ## ## This procedure is invoked to get an RGB triplet ## via 3 RGB slider bars on the FE Color Selector GUI. ## ## Uses that RGB value to set a 'background' color. ## ## Arguments: global variables ## ## CALLED BY: .fRbuttons.buttCOLOR2 button ##+##################################################################### proc set_background_color2 {} { global COLOR2r COLOR2g COLOR2b COLOR2hex ColorSelectorScript aRtext ## FOR TESTING: # puts "COLOR2r: $COLOR2r" # puts "COLOR2g: $COLOR2g" # puts "COLOR2b: $COLOR2b" set TEMPrgb [ exec $ColorSelectorScript $COLOR2r $COLOR2g $COLOR2b] ## FOR TESTING: # puts "TEMPrgb: $TEMPrgb" if { "$TEMPrgb" == "" } { return } scan $TEMPrgb "%s %s %s %s" r255 g255 b255 hexRGB set COLOR2hex "#$hexRGB" set COLOR2r $r255 set COLOR2g $g255 set COLOR2b $b255 ## Set background-and-foreground colors of the indicated color button. update_color_button color2 advise_user "$aRtext(STARTmsg) to use new color." } ## END OF proc 'set_background_color2' ##+##################################################################### ## PROC 'update_color_button' ##+##################################################################### ## PURPOSE: ## This procedure is invoked to set the background color of the ## color button, indicated by the 'colorID' string, ## to its currently set 'colorID' color --- and sets ## foreground color, for text on the button, to a suitable black or ## white color, so that the label text is readable. ## ## Arguments: global color vars ## ## CALLED BY: in two 'set_*_color' procs ## and in the additional-GUI-initialization section at ## the bottom of this script. ##+##################################################################### proc update_color_button {colorID} { global COLOR1r COLOR1g COLOR1b COLOR1hex global COLOR2r COLOR2g COLOR2b COLOR2hex # set colorBREAK 300 set colorBREAK 350 if {"$colorID" == "color1"} { .fRbuttons.buttCOLOR1 configure -bg $COLOR1hex set sumCOLOR1 [expr {$COLOR1r + $COLOR1g + $COLOR1b}] if {$sumCOLOR1 > $colorBREAK} { .fRbuttons.buttCOLOR1 configure -fg "#000000" } else { .fRbuttons.buttCOLOR1 configure -fg "#ffffff" } } elseif {"$colorID" == "color2"} { .fRbuttons.buttCOLOR2 configure -bg $COLOR2hex set sumCOLOR1 [expr {$COLOR2r + $COLOR2g + $COLOR2b}] if {$sumCOLOR1 > $colorBREAK} { .fRbuttons.buttCOLOR2 configure -fg "#000000" } else { .fRbuttons.buttCOLOR2 configure -fg "#ffffff" } } else { ## Seems to be an invalid colorID. return } } ## END OF PROC 'update_color_button' ##+##################################################################### ## PROC 'advise_user' ##+##################################################################### ## PURPOSE: Puts a message to the user on the GUI. ## ## CALLED BY: in three 'set_*_color*' procs, ## in some 'bind' statements in the BIND section above, ## and in the additional-GUI-initialization section at ## the bottom of this script. ##+##################################################################### proc advise_user {text} { .fRmsg.labelINFO configure -text "$text" ## Make sure the text is displayed on the GUI. update ## Alternatively, we could put the message in the title-bar ## of the GUI window. (But it is easy for the user to ## fail to see the message there. Besides, we have more ## options in displaying the message by putting it on a ## Tk widget in the GUI.) ## # wm title . "$text" } ## END OF PROC 'advise_user' ##+######################################################################## ## PROC 'reset_parms' ##+######################################################################## ## PURPOSE: To reset the 'entry' widgets on the GUI ## --- math expression parameters, initial value parameters, ## and two time parameters (endtime and stepsize). ## ## CALLED BY: 'ResetParms' button and in the 'Additional GUI Initialization' ## section at the bottom of this script. ##+######################################################################## proc reset_parms {} { global ENTRYg ENTRYlength \ ENTRYinit1 ENTRYinit2 \ ENTRYendtime ENTRYstepsize set ENTRYg 9.8 set ENTRYlength 10.0 ## Initial angle displacement, in degrees. # set ENTRYinit1 8.0 set ENTRYinit1 80.0 ## Initial anglular velocity, in degrees/sec. set ENTRYinit2 0.0 set ENTRYendtime 20.0 set ENTRYstepsize 0.1 } ## END OF PROC 'reset_parms' ##+#################################################################### ## PROC: 'edit_inputs' ##+##################################################################### ## PURPOSE: Checks entry widgets entries and pops up an error message ## if the data is invalid. ## ## CALLED BY: the 'solve' and 'animate' procs ##+##################################################################### proc edit_inputs {} { ## Decimal/floating-point variables. global ENTRYg ENTRYlength ENTRYinit1 ENTRYinit2 \ ENTRYendtime ENTRYstepsize ## Integer variables. global ENTRYimgsize ## Error indicator. global EDITcode ## We could do without the following EDITcode variable, by using ## a code with the 'return' statement herein. But using this ## code variable is a little more self-documenting. global EDITcode set EDITcode 0 ####################################################### ## Remove trailing and leading blanks (if any) from the ## user entries in the 'entry' widgets. ####################################################### set ENTRYg [string trim $ENTRYg] set ENTRYlength [string trim $ENTRYlength] set ENTRYinit1 [string trim $ENTRYinit1] set ENTRYinit2 [string trim $ENTRYinit2] set ENTRYendtime [string trim $ENTRYendtime] set ENTRYstepsize [string trim $ENTRYstepsize] set ENTRYimgsize [string trim $ENTRYimgsize] ######################################################################### ## Check that these entry fields are NOT blank. ######################################################################### set MSGblank "is blank. Must NOT be blank." if {"$ENTRYg" == ""} { popup_msgVarWithScroll .topErr "The gravitation constant g $MSGblank" +10+10 set EDITcode 1 return } if {"$ENTRYlength" == ""} { popup_msgVarWithScroll .topErr "The pendulum length L $MSGblank" +10+10 set EDITcode 1 return } if {"$ENTRYinit1" == ""} { popup_msgVarWithScroll .topErr "The initial angular displacement $MSGblank" +10+10 set EDITcode 1 return } if {"$ENTRYinit2 " == ""} { popup_msgVarWithScroll .topErr "The initial angular velocity $MSGblank" +10+10 set EDITcode 1 return } if {"$ENTRYendtime" == ""} { popup_msgVarWithScroll .topErr "The end-time for the solve $MSGblank" +10+10 set EDITcode 1 return } if {"$ENTRYstepsize" == ""} { popup_msgVarWithScroll .topErr "The solve step-size $MSGblank" +10+10 set EDITcode 1 return } if {"$ENTRYimgsize" == ""} { popup_msgVarWithScroll .topErr "The size of the image square (pixels) $MSGblank" +10+10 set EDITcode 1 return } ########################################################## ## Check that ENTRYimgsize is an integer. ########################################################## set MSGnotInteger " is NOT INTEGER." if {![string is integer -strict "$ENTRYimgsize"]} { popup_msgVarWithScroll .topErr "The size of the image square (pixels) $MSGnotInteger" +10+10 set EDITcode 1 return } ######################################################################### ## Check that ENTRYg ENTRYlength ENTRYinit1 ENTRYinit2 ENTRYendtime ## and ENTRYstepsize are decimal numbers (positive or negative) --- ## such as 1.234 or -3 or -3.0 or -.4 or .5 or 7 ######################################################################### ## Implemented using the 'decimal_check' proc below. ######################################################################### set NUMERICmsg "should be a decimal number. Examples: 1.234 or 0.56 or -.789" if {![decimal_check "$ENTRYg"]} { popup_msgVarWithScroll .topErr "The gravitational constant g $NUMERICmsg" +10+10 set EDITcode 1 return } if {![decimal_check "$ENTRYlength"]} { popup_msgVarWithScroll .topErr "The pendulum length L $NUMERICmsg" +10+10 set EDITcode 1 return } if {![decimal_check "$ENTRYinit1"]} { popup_msgVarWithScroll .topErr "The initial angular displacement $NUMERICmsg" +10+10 set EDITcode 1 return } if {![decimal_check "$ENTRYinit2"]} { popup_msgVarWithScroll .topErr "The intial angular velocity $NUMERICmsg" +10+10 set EDITcode 1 return } if {![decimal_check "$ENTRYendtime"]} { popup_msgVarWithScroll .topErr "The solve end-time (seconds) $NUMERICmsg" +10+10 set EDITcode 1 return } if {![decimal_check "$ENTRYstepsize"]} { popup_msgVarWithScroll .topErr "The solve step-size (seconds) $NUMERICmsg" +10+10 set EDITcode 1 return } ####################################################################### ## Check that ENTRYg ENTRYlength ENTRYendtime ENTRYstepsize ## are not negative. ####################################################################### set POSITIVEmsg "should be a POSITIVE number. Examples: 1.234 or 0.56" if {$ENTRYg < 0.0} { popup_msgVarWithScroll .topErr "The gravitational constant g $POSITIVEmsg" +10+10 set EDITcode 1 return } if {$ENTRYlength < 0.0} { popup_msgVarWithScroll .topErr "The pendulum length L $POSITIVEmsg" +10+10 set EDITcode 1 return } if {$ENTRYendtime < 0.0} { popup_msgVarWithScroll .topErr "The solve end-time (seconds) $POSITIVEmsg" +10+10 set EDITcode 1 return } if {$ENTRYstepsize < 0.0} { popup_msgVarWithScroll .topErr "The solve step-size (seconds) $POSITIVEmsg" +10+10 set EDITcode 1 return } } ## END of proc 'edit_inputs' ##+######################################################################## ## PROC 'decimal_check' ##+######################################################################## ## PURPOSE: Returns 1 or 0 if the input string looks like a decimal number ## --- positive or negative. Example numbers that are OK: ## 1.234 12.34 0.234 .234 6 ## -1.234 -12.34 -0.234 -.234 -6 ########################################################################### ## References (lots of one-liners): ## http://stackoverflow.com/questions/2072222/regular-expression-for-positive-and-a-negative-decimal-value-in-java ## http://stackoverflow.com/questions/308122/simple-regular-expression-for-a-decimal-with-a-precision-of-2 ## http://stackoverflow.com/questions/4246077/matching-numbers-with-regular-expressions-only-digits-and-commas/4247184#4247184 ## ## More specific to Tcl-Tk (including multi-liners): ## http://wiki.tcl.tk/989 'Regular Expression Examples' ## http://wiki.tcl.tk/768 'Entry Validation' - See "Integer forbidding leading zero:" ## http://wiki.tcl.tk/10166 'string is' ## http://wiki.tcl.tk/40710 'significant digits rounding' - uses regexp to split a number - ## Splits using: if {[regexp {^([+,-]?)([0-9]+)(\.?[0-9]*)?([eE][+-]?[0-9]+)?$} $num -> s i d e]} ## Removes leading zero with: regexp {^(0*)([1-9][0-9]*)$} $i -> NULL DIG ## http://wiki.tcl.tk/530 'Unit converter' has a regexp to parse numbers: ## set RE {(?ix) # Ignore case, extended syntax ## ([-+]?) # Optional leading sign ## ([0-9]*) # Integer part ## \.? # Optional decimal point ## ([0-9]*) # Fractional part ## (e?[0-9]*) # Optional exponent ## } ## ########################################################################### ## I do not mind incurring a little (minute amount of) processing ## with a multiple-line implementation. Probably easier to fix if ## a string gets through --- such as ".0.3" (two decimal points). ## ## CALLED BY: proc 'edit_inputs' ##+######################################################################## proc decimal_check {string} { set PosDecimalOK [regexp {^([0-9]*)\.?([0-9]*)$} "$string"] set NegDecimalOK [regexp {^\-([0-9]*)\.?([0-9]*)$} "$string"] set PosNakedDecimalOK [regexp {^\.?([0-9]*)$} "$string"] set NegNakedDecimalOK [regexp {^\-\.?([0-9]*)$} "$string"] set IntegerOK [string is integer $string] set retCODE [expr { \ $PosDecimalOK || $NegDecimalOK || \ $PosNakedDecimalOK || $NegNakedDecimalOK || \ $IntegerOK }] ## FOR TESTING: if {0} { puts "" puts "decimal_check:" puts "string: $string" puts "PosDecimalOK: $PosDecimalOK" puts "NegDecimalOK: $NegDecimalOK" puts "PosNakedDecimalOK: $PosNakedDecimalOK" puts "NegNakedDecimalOK: $NegNakedDecimalOK" puts "IntegerOK: $IntegerOK" puts "retCODE: $retCODE" } return $retCODE } ## END of proc 'decimal_check' ##+######################################################################## ## PROC 'popup_msgVarWithScroll' ##+######################################################################## ## PURPOSE: Report help or error conditions to the user. ## ## We do not use focus,grab,tkwait in this proc, ## because we use it to show help when the GUI is idle, ## and we may want the user to be able to keep the Help ## window open while doing some other things with the GUI ## such as putting a filename in the filename entry field ## or clicking on a radiobutton. ## ## For a similar proc with focus-grab-tkwait added, ## see the proc 'popup_msgVarWithScroll_wait' in a ## 3DterrainGeneratorExaminer Tk script. ## ## REFERENCE: page 602 of 'Practical Programming in Tcl and Tk', ## 4th edition, by Welch, Jones, Hobbs. ## ## ARGUMENTS: A toplevel frame name (such as .fRhelp or .fRerrmsg) ## and a variable holding text (many lines, if needed). ## ## CALLED BY: 'help' button ##+######################################################################## ## To have more control over the formatting of the message (esp. ## words per line), we use this 'toplevel-text' method, ## rather than the 'tk_dialog' method -- like on page 574 of the book ## by Hattie Schroeder & Mike Doyel,'Interactive Web Applications ## with Tcl/Tk', Appendix A "ED, the Tcl Code Editor". ##+######################################################################## proc popup_msgVarWithScroll { toplevName VARtext ULloc} { ## global fontTEMP_varwidth #; Not needed. 'wish' makes this global. ## global env # bell # bell ################################################# ## Set VARwidth & VARheight from $VARtext. ################################################# ## To get VARheight, ## split at '\n' (newlines) and count 'lines'. ################################################# set VARlist [ split $VARtext "\n" ] ## For testing: # puts "VARlist: $VARlist" set VARheight [ llength $VARlist ] ## For testing: # puts "VARheight: $VARheight" ################################################# ## To get VARwidth, ## loop through the 'lines' getting length ## of each; save max. ################################################# set VARwidth 0 ############################################# ## LOOK AT EACH LINE IN THE LIST. ############################################# foreach line $VARlist { ############################################# ## Get the length of the line. ############################################# set LINEwidth [ string length $line ] if { $LINEwidth > $VARwidth } { set VARwidth $LINEwidth } } ## END OF foreach line $VARlist ## For testing: # puts "VARwidth: $VARwidth" ############################################################### ## NOTE: VARwidth works for a fixed-width font used for the ## text widget ... BUT the programmer may need to be ## careful that the contents of VARtext are all ## countable characters by the 'string length' command. ############################################################### ##################################### ## SETUP 'TOP LEVEL' HELP WINDOW. ##################################### catch {destroy $toplevName} toplevel $toplevName # wm geometry $toplevName 600x400+100+50 # wm geometry $toplevName +100+50 wm geometry $toplevName $ULloc wm title $toplevName "Note" # wm title $toplevName "Note to $env(USER)" wm iconname $toplevName "Note" ##################################### ## In the frame '$toplevName' - ## DEFINE THE TEXT WIDGET and ## its two scrollbars --- and ## DEFINE an OK BUTTON widget. ##################################### if {$VARheight > 10} { text $toplevName.text \ -wrap none \ -font fontTEMP_fixedwidth \ -width $VARwidth \ -height $VARheight \ -bg "#f0f0f0" \ -relief raised \ -bd 2 \ -yscrollcommand "$toplevName.scrolly set" \ -xscrollcommand "$toplevName.scrollx set" ## -font fontTEMP_varwidth scrollbar $toplevName.scrolly \ -orient vertical \ -command "$toplevName.text yview" scrollbar $toplevName.scrollx \ -orient horizontal \ -command "$toplevName.text xview" } else { text $toplevName.text \ -wrap none \ -font fontTEMP_fixedwidth \ -width $VARwidth \ -height $VARheight \ -bg "#f0f0f0" \ -relief raised \ -bd 2 ## -font fontTEMP_varwidth } button $toplevName.butt \ -text "OK" \ -font fontTEMP_varwidth \ -command "destroy $toplevName" ############################################### ## PACK *ALL* the widgets in frame '$toplevName'. ############################################### ## Pack the bottom button BEFORE the ## bottom x-scrollbar widget, pack $toplevName.butt \ -side bottom \ -anchor center \ -fill none \ -expand 0 if {$VARheight > 10} { ## Pack the scrollbars BEFORE the text widget, ## so that the text does not monopolize the space. pack $toplevName.scrolly \ -side right \ -anchor center \ -fill y \ -expand 0 ## DO NOT USE '-expand 1' HERE on the Y-scrollbar. ## THAT ALLOWS Y-SCROLLBAR TO EXPAND AND PUTS ## BLANK SPACE BETWEEN Y-SCROLLBAR & THE TEXT AREA. pack $toplevName.scrollx \ -side bottom \ -anchor center \ -fill x \ -expand 0 ## DO NOT USE '-expand 1' HERE on the X-scrollbar. ## THAT KEEPS THE TEXT AREA FROM EXPANDING. pack $toplevName.text \ -side top \ -anchor center \ -fill both \ -expand 1 } else { pack $toplevName.text \ -side top \ -anchor center \ -fill both \ -expand 1 } ################################################ ## Set some 'event' bindings to allow for ## easy scrolling through huge listings. ## is a press of the Page-Down key. ## is a press of the Page-Up key. ## is a press of the Home key ## to go to the top of the listing. ## is a press of the End key ## to go to the bottom of the listing. ## is a press of the Up-arrow key. ## is a press of the Down-arrow key. ################################################ bind $toplevName "$toplevName.text yview scroll +1 page" bind $toplevName "$toplevName.text yview scroll -1 page" bind $toplevName "$toplevName.text see 1.0" bind $toplevName "$toplevName.text see end" bind $toplevName "$toplevName.text yview scroll -1 unit" bind $toplevName "$toplevName.text yview scroll +1 unit" ##################################### ## LOAD MSG INTO TEXT WIDGET. ##################################### ## $toplevName.text delete 1.0 end $toplevName.text insert end $VARtext $toplevName.text configure -state disabled } ## END OF PROC 'popup_msgVarWithScroll' ##+######################################################## ## Set the 'HELPtext' var. ##+######################################################## set HELPtext \ " ** HELP for this 'Simulate Single Pendulum with No Friction' ** * software application * (which includes an option to animate the pendulum on a 'canvas') This Tk GUI script solves the second-order differential equation for the oscillations of a single pendulum --- by using numerical integration (the Runge-Kutta method). The ODE (ordinary differential equation) for a single, fixed-pivot-point pendulum arm with no frictional forces is D(D(u)) = -g/L * sin(u) where D represents the time-derivative operator d/dt, and t represents the time independent variable, and u represents the angular displacement of the pendulum arm from vertical, and g is the acceleration due to gravity (on Earth or the Moon or Mars or a mountain-top or whatever), and L is the length of the pendulum arm (the distance to a heavy mass at the end of the arm, where the mass is so great that the weight of the arm is negligible --- or L is the distance from the fixed-pivot-point to the center-of-mass of the arm and of the mass at the end of the arm). This 'nonlinear' form of the right-hand-side allows for simulating a widely-swinging (significantly displaced) pendulum. If one considers a pendulum swinging with small angular displacement, we get a good approximation to the motion by using the ODE D(D(u)) = -g/L * u because u is a good approximation to sin(u) when u is small. The general solution of this ODE is of the form A * sin(w*t) + B * cos(w*t) where w = sqrt(g/L) and where the constants A and B can be determined from two initial conditions (inital angular displacement and initial angular velocity). '2pi/w' is the period T of the oscillation where T is in units like seconds per cycle. The reciprocal of T is the cycles per second --- the frequency. So the frequency, f, is w/2pi. Thus the solution can be written A * sin(2pi*f*t) + B * cos(2pi*f*t) or A * sin(2pi*t/T) + B * cos(2pi*t/T) An expression of the form A * sin(k*t) + B * cos(k*t) can be written in the form C * sin(k*t + D) where the constant D is called a 'phase angle'. Hence the solutions to the 'linear' pendulum differential equations are sinusoidal. The solutions of the 'nonlinear' pendulum equation can be expected to be similar to a sinusoidal function --- but with the graph against time somewhat distorted. ************ GUI FEATURES: ************ The GUI allows the user to enter various values for g and L. To get meaningful results, the units for g and L should be compatible --- for example: g = 9.80665 meters-per-second-per-second L = 10.0 meters or g = 32.174 feet-per-second-per-second L = 30 feet (Note that the equation for angular acceleration does not involve mass --- the mass factors cancelled out during derivation of the equation. So the angular motion of the pendulum should be the same whether the mass is 10 kg or 20 kg, for example.) The GUI also allows the user to enter parameters for the solver process: - an initial angular displacement from vertical - an initial angular velocity (typically zero) - an end-time for the end of the solution process - a time-step, h, for the solution process. Following a solution run, the GUI also allows the user to start (and stop) an animation of a pendulum drawn on a Tk canvas. The animation is shown on a rectangular Tk 'canvas' widget by using 'create line' and 'create oval' and 'delete' commands on the canvas. ************************* EXPERIMENTING VIA THE GUI: ************************* It was indicated above that the frequency of oscillations of the pendulum is proportional to sqrt(g/L). Therefore, to make the pendulum oscillate more quickly, the user can increase g or decrease L. And to make the pendulum oscillate more slowly, the user can decrease g or increase L. One could experiment with g to compare how fast a pendulum of given length would oscillate on the surface of a planet like Jupiter or Mars or Saturn --- versus on Earth. Here is a table of approximate gravitational acceleration values on the surface of several large spherical celestial bodies: Planet g (meters/sec/sec) --------- ------------------ Mercury 3.8 Venus 8.8 Earth 9.8 Mars 3.8 Jupiter 25 Saturn 10.4 Uranus 10.4 Neptune 13.8 Moons g (meters/sec/sec) ---------------- ------------------ Earth's Moon 1.62 Jupiter's Callisto 1.23 Jupiter's Europa 1.31 Jupiter's Ganymede 1.43 Jupiter's Io 1.80 Saturn's Titan 1.35 One source of values like these is Wikipedia. ************************************************* METHOD of MATH MODELLING OF THE PENDULUM MOTION: ************************************************* We convert the single 'second order' differential equation D(D(u)) = -g/L * sin(u) to two 'first order' differential equations with u1 = u and u2 = D(u) as the functions of t to be generated by integrating 2 differential eqns: D(u1) = u2 D(u2) = -g/L * sin(u1) for initial conditions u1=A and u2=B, where A is an initial angle and B is an initial angular velocity. The common way of expressing these kinds of systems of first order differential equations in compact, general form is D(u) = f(t,u) where u and f are N-dimensional vectors. This is a compact way of expressing a system of scalar differential equations: D(u1) = f1(t,u1,...,uN) D(u2) = f2(t,u1,...,uN) ...... D(uN) = fN(t,u1,...,uN) In the case of these pendulum equations, N=2, and we can think of solving for the unknown function vector (u1(t),u2(t)) where the right-hand-side (RHS) of the two equations above can be thought of as a special case of a more general user-specified function vector (f1(t,u1,u2),f2(t,u1,u2)) where f1(t,u1,u2) = u2 f2(t,u1,u2) = -g/L * sin(u1) We use the popular Runge-Kutta 4th order method (RK4) to perform the numerical integration for a user-specified time step, h. We basically use two utility procs to perform the integration steps: - a proc to perform the RK4 integration for N=2 --- giving the values of (u1,u2) for each time step - a proc to evaluate the RHS function: (f1,f2) --- for specified values of t,u1,u2. The latter proc is called several times by the former proc for each time step. *************************************************** METHOD of PLOTTING THE ANIMATION on the TK CANVAS: *************************************************** After a solution, we have the solution functions u1 and u2 for a sequence of equally-spaced time values. We use function u1 to do the animation. For each time value, t(i), the pendulum is drawn as a simple - line-segment representing the 'arm' of the pendulum - color-filled circle representing the mass at the end of the (essentially weight-less) arm. The GUI provides 2 buttons by which the user can specify the 2 colors for: - the canvas background - the pendulum components (line and circle). An 'animate' proc performs the pendulum animation when the user clicks on the 'Start' radiobutton of the GUI. This animate proc uses the 'world-coordinates' --- the values of angle u1 --- to draw the swinging pendulum within an area of about 2*L by 2*L in world coordinates where L is the length of the pendulum arm. We think of the pivot-point of the pendulum as being at the origin --- (0.0,0.0). We use the angle u1 to compute the x,y coordinates of the end of the pendulum arm (and center of the mass, represented by a circle). The 2Lx2L area allows for the pendulum to swing to extremes --- a horizontal arm to the left and right --- and a vertical arm below (zero degrees) the pivot-point or above (plus or minus 180 degrees) the pivot-point. A proc is provided which maps the plot area limits in world coordinates --- say UpperLeftCorner: (-L,+L) LowerRightCorner: (+L,-L) to the corners of the plot area in pixel coordinates: UpperLeftCorner: (0,0) LowerRightCorner: (ImageWidthPx,ImageHeightPx). We use a value a little larger than L for the world coordinate limits --- to allow for a little margin around the swinging pendulum. To get a square image area, we use ImageWidthPx=ImageHeightPx and we determine this number of pixels by allowing the user to specify the integer value in an entry widget on the GUI. The animate proc uses 2 procs --- Xwc2Xpx and Ywc2Ypx --- to convert the world coordinates of each point --- such as the end-points of the pendulum arm --- to pixel coordinates. The pixel-coordinates are used in Tk canvas 'create line' and 'create oval' commands to redraw the pendulum-arm and the attached mass (represented by a circle) for each time step. " ##+##################################################### ##+##################################################### ## ADDITIONAL GUI INITIALIZATION, if needed (or wanted). ##+##################################################### ##+##################################################### ##+##################################################### ## Set the full-name of the RGB color-selector Tk script ## that is used in several procs above. ##+##################################################### ## FOR TESTING: # puts "argv0: $argv0" set DIRthisScript "[file dirname $argv0]" ## For ease of testing in a Linux/Unix terminal and located at the ## directory containing this Tk script. Set the full directory name. if {"$DIRthisScript" == "."} { set DIRthisScript "[pwd]" } set DIRupOne "[file dirname "$DIRthisScript"]" set DIRupTwo "[file dirname "$DIRupOne"]" set ColorSelectorScript "$DIRupTwo/SELECTORtools/tkRGBselector/sho_colorvals_via_sliders3rgb.tk" ## Alternatively: Put the RGB color-selector Tk script in the ## same directory as this Tk script and uncomment the following. # set ColorSelectorScript "$DIRthisScript/sho_colorvals_via_sliders3rgb.tk" ##+############################################# ## Initialize the pendulum-color. ## ## (Change 'if {1}' to 'if {0}' to try an ## alternative pendulum-color.) ##+############################################# if {1} { ## Yellow: set COLOR1r 255 set COLOR1g 255 set COLOR1b 0 } else { ## White: set COLOR1r 255 set COLOR1g 255 set COLOR1b 255 } set COLOR1hex [format "#%02X%02X%02X" $COLOR1r $COLOR1g $COLOR1b] update_color_button "color1" ##+############################################# ## Initialize the background-color. ## ## (Change 'if {1}' to 'if {0}' to try an ## alternative min-color.) ##+############################################# if {1} { ## Magenta: set COLOR2r 255 set COLOR2g 0 set COLOR2b 255 } else { ## Red: set COLOR2r 255 set COLOR2g 0 set COLOR2b 0 } set COLOR2hex [format "#%02X%02X%02X" $COLOR2r $COLOR2g $COLOR2b] update_color_button "color2" ##+################################################# ## Set constants to use for angle calculations --- ## in radians (and degrees). ##+################################################ set pi [expr {4.0 * atan(1.0)}] set twopi [expr {2.0 * $pi}] set radsPERdeg [expr {$pi/180.0}] #+####################################################### ## Initialize the entry widgets on the GUI. ##+###################################################### reset_parms ################################################################## ## Set the radius of the circle that will represent the mass ## at the end of the pendulum arm --- in pixels --- for ## use in 'create oval' commands in the 'animate' proc. ################################################################## # set radiusPx 3 set radiusPx 6 ################################################################## ## Set the wait-millisecs between each draw of the pendulum ## (arm and mass) in the 'animate' proc. ## (To be used if we activate the entry widget for ## ENTRYmillisecs.) ################################################################## # set ENTRYmillisecs 100 ################################################################## ## Initialize the image-size entry field. ################################################################## set ENTRYimgsize 300 ##+##################################################### ## Advise the user how to start. ##+##################################################### advise_user "$aRtext(SOLVEmsg)"