[EKB] This gives an example of what I think is a particularly Tcl-ish use of [Model / View / Controller]. I use this frequently in my own work and it certainly makes it easier to incrementally develop an application. The architecture is pretty basic, something like this: ====== +--------------------+ | | | Core Program | | | +---------+----------+ | | / \ / \ / \ Mini- GUI(s) language/ CLI ====== The core program has no user interface capability. It just has programmatic hooks. In MVC, it is the model. Usually I use [Snit] for this. Because this is Tcl, building the model, as a program with hooks, is basically a mini-language. It is easy to implement using a command-line program that just gives access to the program. The program itself is the controller and the command-line is the view (at least, I think this is a correct interpretation of MVC). Although I often don't have to go to the next step, the MVC pattern makes it very easy to access the model via a Tk GUI, which would involve a further controller (a layer between the GUI code and the model) and the view (the GUI itself). ---- **An Example** ***Command-line controller*** First, here's the command-line controller, to show how basic it is. It just calls the core program, which is inside the two files "crop.tcl" and "yield.tcl": ====== package require snit source "crop.tcl" source "yield.tcl" if {$argc != 1} { puts "Usage:" puts " yield inputfile.yld" puts "" puts "Your file is \"inputfile.yld\". It does not have" puts "to have a .yld extension, but must have all of" puts "the elements required for a yield input file." exit } namespace eval yield { source [lindex $argv 0] } # Run it! yield::run ====== ***Model*** The model itself consists of two files: ****yield.tcl**** ====== namespace eval yield {} { namespace export start land irrigation getdata variable land set land(Ks) 6.0 set land(S) 1000.0 set land(fLAI) 0.1 set land(fKc) 0.1 set land(z0) 0.5 set land(ARNOb) 0.5 set land(WEAPro) LAI set land(PAW) 0.15 set land(runoff) WEAP variable irr set irr(eff) 0.0 set irr(lthresh) 0.5 set irr(uthresh) 0.9 set irr(Novak) 0.463 ;# Novak parameter for LAI dependence of T vs E variable inputs variable start variable crops {} } proc yield::land {args} { variable land foreach {arg val} $args { switch -- $arg { -conductivity {set land(Ks) $val} -maxsoilcap {set land(S) $val} -fallowLAI {set land(fLAI) $val} -fallowKc {set land(fKc) $val} -satfrac {set land(z0) $val} -soilmoistureshape { set land(ARNOb) $val set land(runoff) ARNO } -runoffresistance { set land(WEAPro) $val set land(runoff) WEAP } -plantavailwater {set land(PAW) $val} default { error "Values can be one of: [join [lsort [array names $land]] ,]" } } } } proc yield::irrigation {args} { variable irr foreach {arg val} $args { switch -- $arg { -efficiency {set irr(eff) $val} -lowerthreshold {set irr(lthresh) $val} -upperthreshold {set irr(uthresh) $val} -novak {set irr(Novak) $val} default { error "Values can be one of: [join [lsort [array names $irr]] ,]" } } } } proc yield::outfile {s} { variable file set file(output) $s return $s } # The args give the order in which the variables are entered # The names *must* be I, E, or P (for Irrigation, ET0, and Precip) # The first line of the file is ignored if it has text # E.g.: getdata myfile I E P proc yield::getdata {infile args} { variable inputs if [catch {open $infile r} fhndl] { error "Could not open file $file(input): $fhndl" return } # Ignore first line if it has text; otherwise, read it gets $fhndl line if {![regexp -- {[A-Za-z]} $line]} { # Rewind to first line seek $fhndl 0 } set inputs(I) {} set inputs(E) {} set inputs(P) {} while {[gets $fhndl line] >= 0} { foreach item $line lname $args { lappend inputs($lname) $item } } close $fhndl } proc yield::start {crop d} { variable start variable crops regexp {(\d+)(\+(\w+))?} $d -> day + refcrop if {$refcrop ne ""} { set day [expr {$start($refcrop) + [::$refcrop LGP] + $day}] } set start($crop) $day lappend crops $crop } proc yield::run {{outfile ""}} { variable crops variable run variable inputs variable land variable irr variable start if {$outfile eq ""} { set fhndl stdout } else { if [catch {open $outfile w} fhndl] { error "Cannot open file $outfile: $fhndl" } } set croplist [join $crops " Yield\t"] puts $fhndl "Day\tIrrigation Depth\t$croplist Yield\tRoot Zone Storage (frac.)" set i 0 set z $land(z0) set irrigating false foreach ET0 $inputs(E) P $inputs(P) Iavail $inputs(I) { # Note: This assumes cropping periods do not overlap set Kc $land(fKc) set LAI $land(fLAI) set fallow true foreach crop [array names start] { set inGrowingPeriod($crop) [expr {$i > $start($crop) && $i < $start($crop) + [::$crop LGP]}] if $inGrowingPeriod($crop) { set Kc [::$crop Kc] set LAI [::$crop LAI] set rootdepth [::$crop rootdepth] set fallow false } } if {$land(runoff) eq "ARNO"} { set PHI [expr {$P - $ET0 * $Kc}] if {$PHI <= 0} { set runoff 0 } else { set temp1 [expr {1.0 * $land(S)/($land(ARNOb) + 1.0)}] # Note that z should not be greater than 1: but catch anyway set temp2 [expr {$z >= 1 ? 0.0 : pow(1.0 - $z, $land(ARNOb) + 1.0)}] if {$PHI < $land(S) * (1.0 - $z)} { set runoff [expr {$PHI - $temp1 * ($temp2 - pow(1.0 - $z - 1.0 * $PHI/$land(S), $land(ARNOb) + 1.0))}] } else { set runoff [expr {$PHI - $temp1 * $temp2}] } } } else { # If not ARNO, using WEAP (set by default) if {$land(WEAPro) eq "LAI"} { set k $LAI } else { set k $land(WEAPro) } set runoff [expr {($P + $Iavail) * ($z >= 1 ? 1.0 : pow(max(0, $z), $k))}] } set ET [expr {$ET0 * $Kc * (5.0 * $z - 2.0 * $z * $z)/3.0}] set ETpart [expr {exp(-$irr(Novak) * $LAI)}] set irred 1 if {1.0 - (1.0 - $irr(eff)) * $ETpart > 1.0e-7} { set irrred [expr {(1.0 - $ETpart) / (1.0 - (1.0 - $irr(eff)) * $ETpart)}] } if $fallow { set irrigating false } else { # Convert to: water depth, millimeters set rdwater [expr {1000 * $rootdepth * $land(PAW)}] if {$z * $land(S) < $irr(lthresh) * $rdwater} { set irrigating true } else { if {$z * $land(S) > $irr(uthresh) * $rdwater} { set irrigating false } } } if {$irrigating} { set Ineed [expr {$irr(uthresh) * $rdwater - $z *$land(S)}] set Iwithd [expr {$irred * $Ineed > $Iavail ? $Iavail : $irred * $Ineed}] if {$Iavail < 1.0e-7} { set I 0.0 } else { set I [expr {$Ineed * (1.0 * $Iwithd/$Iavail)}] } } else { set I 0.0 } set zprov [expr {$z + ($P + $I - $runoff - \ $ET - $land(Ks) * $z * $z) / \ (1.0 * $land(S))}] # Keep in bounds for extreme values set z [expr {min(1.0, max($zprov, 0.0))}] foreach crop [array names start] { if $inGrowingPeriod($crop) { ::$crop grow $ET $ET0 } } incr i set outline [format "%d\t\%4.4f" $i [expr {$irred * $I}]] foreach crop $crops { set outline [format "%s\t%5.4f" $outline [::$crop yield]] } set outline [format "%s\t%0.4f" $outline $z] puts $fhndl $outline } # Close the file or, if stdout, flush close $fhndl } ====== ****crop.tcl**** ====== snit::type crop { option -days {1 1 1 1} option -kc {1 1 1} option -ky {1 1 1 1} option -rootdepth {0.3 1} ;# In meters option -minlai 1 option -maxlai 5 option -maxyield 1 variable potyield variable currday variable currstage variable cumET variable cumPET constructor {args} { $self configurelist $args $self reset } typevariable stages [list \ {Initial} \ {Crop Development} \ {Mid-Season} \ {Late} \ ] method yield {} { variable potyield return $potyield } method day {} { variable currday return $currday } method reset {} { variable potyield variable currday variable currstage variable cumET variable cumPET set potyield [$self cget -maxyield] set currday 0 set currstage 1 set cumET 0 set cumPET 0 } # This is the main method: # increase day by 1 # set new internal variables if needed method grow {ET ET0} { variable potyield variable currday variable currstage variable cumET variable cumPET set cumET [expr {$cumET + $ET}] set cumPET [expr {$cumPET + [$self Kc $currday] * $ET0}] incr currday if {[$self stage $currday] > $currstage} { set ymax [$self cget -maxyield] set Ky [lindex [$self cget -ky] [expr {$currstage - 1}]] set tempyield [expr {$ymax * (1.0 - $Ky * (1.0 - 1.0 * $cumET/$cumPET))}] set potyield [expr {$potyield < $tempyield ? $potyield : $tempyield}] incr currstage # Reset variables set cumET 0 set cumPET 0 } } # Get the stage (1-4) for the day (from start of growing season) method stage {{d -1}} { variable currday if {$d == -1} { set d $currday } set stage 0 set totdays 0 foreach stagedays [$self cget -days] { incr stage incr totdays $stagedays if {$totdays > $d} { break } } return $stage } method LGP {} { set LGP 0 foreach days [$self cget -days] { incr LGP $days } return $LGP } method stagename {{d -1}} { variable currday if {$d == -1} { set d $currday } set stage [$self stage $d] incr stage -1 return [lindex $stages $stage] } method startofstage {n} { set stagedays [$self cget -days] set startofstage 0 for {set i 0} {$i < $n - 1} {incr i} { incr startofstage [lindex $stagedays $i] } return $startofstage } method Ky {{d -1}} { variable currday if {$d == -1} { set d $currday } set stage [$self stage $d] incr stage -1 return [lindex [$self cget -ky] $stage] } method rootdepth {{d -1}} { variable currday if {$d == -1} { set d $currday } set rd0 [lindex [$self cget -rootdepth] 0] set rd1 [lindex [$self cget -rootdepth] 1] set stage [$self stage $d] set stagedays [$self cget -days] set s12days [expr {[lindex $stagedays 0] + [lindex $stagedays 1]}] switch $stage { 1 - 2 { return [expr {$rd0 + 1.0 * ($rd1 - $rd0) * $d/$s12days}] } 3 - 4 { return $rd1 } default { error "Invalid stage: $stage" } } } method LAI {{d -1}} { variable potyield variable currday if {$d == -1} { set d $currday } set stage [$self stage $d] set minlai [$self cget -minlai] set maxlai [$self cget -maxlai] set maxyield [$self cget -maxyield] # Adjust maxlai set maxlai [expr {$minlai + ($maxlai - $minlai) * (1.0 * $potyield)/$maxyield}] # Are we already past the end of the growing period? Back to background level if {$d > [$self LGP]} { return $minlai } switch $stage { 1 { return $minlai } 2 { set delta [expr {$d - [$self startofstage $stage]}] set tot [lindex [$self cget -days] [expr {$stage - 1}]] return [expr {$minlai + (1.0 * $delta/$tot) * ($maxlai - $minlai)}] } 3 { return $maxlai } 4 { set delta [expr {$d - [$self startofstage $stage]}] set tot [lindex [$self cget -days] [expr {$stage - 1}]] return [expr {$maxlai + (1.0 * $delta/$tot) * ($minlai - $maxlai)}] } default { error "Invalid stage: $stage" } } } method Kc {{d -1}} { variable potyield variable currday if {$d == -1} { set d $currday } set maxyield [$self cget -maxyield] set f [expr {1.0 - [$self Ky $d] * (1.0 - 1.0 * $potyield/$maxyield)}] set stage [$self stage $d] for {set i 0} {$i < 3} {incr i} { set Kc$i [expr {$f * [lindex [$self cget -kc] $i]}] } # Are we already past the end of the growing period? if {$d > [$self LGP]} { 0 } switch $stage { 1 { return $Kc0 } 2 { set delta [expr {$d - [$self startofstage $stage]}] set tot [lindex [$self cget -days] [expr {$stage - 1}]] return [expr {$Kc0 + (1.0 * $delta/$tot) * ($Kc1 - $Kc0)}] } 3 { return $Kc1 } 4 { set delta [expr {$d - [$self startofstage $stage]}] set tot [lindex [$self cget -days] [expr {$stage - 1}]] return [expr {$Kc2 + (1.0 * $delta/$tot) * ($Kc2 - $Kc1)}] } default { error "Invalid stage: $stage" } } } } ====== ---- !!!!!! %| enter categories here |% !!!!!!