Arjen Markus (2 june 2004) I have been looking for ways to approach physical problems using Tcl, but most of the interesting problems I could come with, are governed by complicated partial differential equations, such as the Navier-Stokes equations for fluid dynamics and Schroedinger's equation for quantum mechanics (the latter is not my area of expertise anyway :)).
The solution of such equations requires much more work than I can do in, say, one or two evenings.
Other simpler problems, from mechanics to thermodynamics, often involve setting up a momentum or heat balance and then solving that for a particular situation. And that is not easily captured in a static script either.
Well, that might not be entirely true, as shown below for the problem of two point masses (small balls, if you like) that collide head on. The equations that will give us the velocities after the collision are simple:
Nevertheless, we get a set of equations with two unknowns and one of the equations is quadratic.
So how do we set up these equations and solve them in the laziest way possible? Let us look at the momentum balance:
total momentum = momentum ball 1 + momentum ball 2
where:
momentum = mass * velocity
All we have to do is minimize the function:
func = (total momentum after - total momentum before) ** 2
for the two velocities to make sure we "conserve" momentum. This is achieved at the minimum we are looking for. Then the function attains a value 0 and the total momentum before and after is the same.
What we do not want to do - at least I do not - is fill in all the formulae to make one big thing. It is so easy to make mistakes ...
And here is where Tcl's traces come in very handy:
If you need the momentum to compute the above function, then let the trace mechanism invoke a small procedure to calculate that momentum.
To make this idea a bit clearer, suppose we have a block of a certain length, width and height. Then its volume is:
volume = length * width * height
If you change the width, you will need to recalculate the volume when next you need it. Now, if you put a trace on the variable "volume", so that it gets computed when you ask for its value, like:
set width 2.0 puts $volume
you have automated the process!
That is done in the script below. Some remarks:
# phys.tcl -- # Solve equations without too much fuss, so that # certain physical problems become a piece of cake # namespace eval ::phys { namespace export const var solve } # const -- # Define a variable that does not depend on others # Arguments: # vname Name of the variable # value (Optional) value # Result: # New variable (the one in the caller's scope is linked to the private one) # proc ::phys::const {vname args} { variable $vname uplevel [list upvar 0 ::phys::$vname $vname] if { $args != {} } { set $vname [lindex $args 0] return [set $vname] } } # var -- # Define a variable that _does_ depend on others # Arguments: # vname Name of the variable # vexpr Expression to use when the value is required # Result: # New variable (the one in the caller's scope is linked to the private one) # proc ::phys::var {vname vexpr} { variable $vname uplevel [list upvar 0 ::phys::$vname $vname] set body { variable VNAME set VNAME [namespace eval ::phys expr {VEXPR}] } proc ::phys::__set_$vname {vname1 vname2 op} \ [string map [list VNAME $vname VEXPR $vexpr] $body] trace add variable $vname read ::phys::__set_$vname } # solve -- # Solve a set of equations as defined by one or more variables # Arguments: # equations Names of the variables that define the equations # variables Names of the variables for which to solve them # startvalues (Optional) the initial values of these variables # Result: # The variables are set to their new values, approximating a solution # the solution is printed on the screen # proc ::phys::solve { equations variables {startvalues {}} } { # # Set the initial values # if { $startvalues != {} } { foreach v $variables val $startvalues { set ::phys::$v $val } } else { foreach v $variables { lappend startvalues [set ::phys::$v] } } # # Construct a temporary procedure that defines the # function we will minimize: # # 1. The variable commands # set ns_vars "" foreach v $equations { append ns_vars "variable $v\n" } # # 2. The function: the sum of squares of each equation # set eqs 0.0 foreach e $equations { append eqs "+\$$e*\$$e" } # # 3. The procedure # proc ::phys::__minfunc args "$ns_vars; \ foreach v [list $variables] val \$args {set ::phys::\$v \$val} ; \ expr {$eqs}" # # Now, minimize the function and transfer the result to the variables # set result [::math::optimize::minimumSteepestDescent ::phys::__minfunc $startvalues] foreach v $variables val $result { set ::phys::$v $val puts "$v = $val" } } # # A simple problem: the volume of a block # namespace import ::phys::* source differentiate.tcl const width 10.0 const length 3.0 const height 7.0 var volume {$width*$height*$length} puts "Volume: $volume" set width 4.0 puts "Volume: $volume" # # Now something less trivial: # Calculate the width of a block with given volume, height and length # var dvol {$volume-100.0} solve dvol width 1 # # Just for the fun of it: # Compute the velocities after a frontal collision of # two point bodies with equal mass (to make the equations simpler) # The trick: # - the momentum before and after the collision must be the same # - the kinetic energy must be the same # const start_vel1 1.0 const start_vel2 0.1 const vel1 const vel2 var start_momentum {$start_vel1+$start_vel2} var start_kin_energy {$start_vel1*$start_vel1+$start_vel2*$start_vel2} var end_momentum {$vel1+$vel2} var end_kin_energy {$vel1*$vel1+$vel2*$vel2} var dmom {$end_momentum-$start_momentum} var dkin {$end_kin_energy-$start_kin_energy} solve {dmom dkin} {vel1 vel2} {0.0 10.0} # # What remains to be done: check that we have indeed # found an acceptable solution! #