Arjen Markus (21 may 2019) Using the various packages available in Tcllib and Tklib it is pretty easy to build a simulator like the one shown in the code below. The idea is simple:
Of course there are a few tricks involved:
And because I was too lazy to build a more conventional GUI, I made it such that things start when you change a slider.
I only hope I have implemented the simulation part correctly ;).
Note 1: It is a self-contained program, there is no attempt to read the VCD files (value change dump) that are typically used in this context.
Note 2: An alternative mode of operation could be to let the simulation continue and update the parameters "on the fly". The type of plot would have to be changed to a stripchart then.
# controlled_oscillator.tcl -- # Experiment with a simple linear oscillator: # - Two degrees of freedom - the spring parameter and the damping coefficient. # These are simply set to a fixed value. # - The oscillator itself receives a random external force consisting of # small peaks # - The PID-controller has three parameters, P, I and D that can be # changed via sliders in the GUI. # # Note: # This could be set up in a more modular way, by using OOP. # package require controlwidget package require Plotchart package require math::calculus # integrate -- # Integrate the equations and display the result # # Arguments: # None # proc integrate {} { global springStrength global dampingCoeff global force global integratedX global tstart global tinterval global tduration set time 0.0 set tstep 0.1 set tcount 0 set tstart 0.0 set tinterval 0.3 set tduration 0.15 set springStrength 0.5 set dampingCoeff 0.2 set force 0.0 set integratedX 0.0 # # Initial conditions: # x, u - the uncontrolled position and velocity # xc, uc - the controlled position and velocity # set x 0.0 set u 0.0 set xc $x set uc $u while { $time < 200.0 } { lassign [::math::calculus::rungeKuttaStep $time $tstep [list $x $u $xc $uc] oscillator] x u xc uc #puts "$time\t$x\t$u" if { $tcount == 0 } { $::p plot data1 $time $x $::p plot data2 $time $xc } incr tcount if { $tcount == 10 } { set tcount 0 } set time [expr {$time + $tstep}] set integratedX [expr {$integratedX + $tstep * $xc}] ;# Do it outside the RK integration step! } } # oscillator -- # Calculate the right-hand side of the equations # # Arguments: # time Time in the simulation # coords State variables # # Result: # Vector representing the right-hand side # proc oscillator {time coords} { global springStrength global dampingCoeff lassign $coords x u xc uc set f [externalForce $time] set g [pidSteering $xc $uc] set du [expr {-$springStrength * $x - $dampingCoeff * $u + $f}] set duc [expr {-$springStrength * $xc - $dampingCoeff * $uc + $f + $g}] set dx $u set dxc $uc return [list $dx $du $dxc $duc] } # externalForce -- # Calculate the external force as a series of random peaks # # Arguments: # time Time in the simulation # # Result: # 1.0 or 0.0 depending on the time and the random function # proc externalForce {time} { global tstart global tinterval global tduration global force global integratedX if { $time >= $tstart + $tinterval } { set tstart $time set r [expr {rand()}] set force [expr {($r > 0.9)? 1.0 : 0.0}] } else { if { $time > $tstart + $tduration } { set force 0.0 } } return $force } # pidSteering -- # Calculate the steering force using PID # # Arguments: # x X coordinate (excursion from 0) # u Velocity # # Result: # Steering force # proc pidSteering {x u} { global integratedX global kp global ki global kd set g [expr {- $kp * $x - $ki * $integratedX - $kd * $u }] return $g } # showValues -- # Show the chosen values as text and run the simulation if opportune # # Arguments: # var1 Name of the traced variable/array # var2 Name of the element if an array is traced # op The operation (always write in this case) # proc showValues {var1 var2 op} { global afterid lassign [set $var1] P I D set ::slidertext [format "P:\t%.2f\nI:\t%.2f\nD:\t%.2f" $P $I $D] if { $afterid != {} } { after cancel $afterid } set afterid [after 100 updateParameters] } # updateParameters -- # Update the parameters and re-run the calculation # proc updateParameters {} { global kp global ki global kd lassign $::slidervar kp ki kd $::p deletedata expr {srand(1000)} integrate } # setupGUI -- # Set up the GUI, consisting of the controls for the PID parameters # and the graph # # Arguments: # None # proc setupGUI {} { global afterid global slidervar global slidertext global p set afterid {} grid [::controlwidget::slider .slider -variable slidervar -from 0.0 -to 2.0 -number 3 -axisformat %.1f -axiscolor green -background white -height 300] \ [canvas .c -width 800 -height 500] grid [::ttk::label .slidervars -textvariable slidertext] ^ set p [::Plotchart::createXYPlot .c {0 200 50} {-2.0 2.0 0.5}] $p dataconfig data1 -colour red $p dataconfig data2 -colour green $p legend data1 "Uncontrolled signal" $p legend data2 "Controlled signal" set slidervar {0.0 0.0 0.0} set slidertext "Change\nthe\nsliders" trace add variable slidervar write showValues } # Start the GUI # setupGUI