Arjen Markus (24 december 2008) Here is a little experiment with a framework for simulating a system made up of components. The idea is that the components do not "know" of each other, other than via the data they provide. The central part of the framework keeps track of time and all the data, as well as which components are available. Computations are done only if a component asks for the associated resource at a particular time
The example is simply a room that is being heated by a single heater. If the temperature is above the set temperature for that time of day, the heating is turned off. As it is pretty cold outside, the heater is turned on regularly. (Note: the numerical values do not represent any real situation - they are merely very rough estimates)
As for the structure:
This experiment is inspired by various publications on the subject of simulation frameworks. But most such published frameworks are developed in Java or C++. For me, Tcl offers a much more flexible approach, even if the interface in the program below is far from ideal. Well, I hope to come up with a "Mark II" soon. Oh, and because I wanted to get it done quickly, I did not bother to study these articles in detail - I do not know yet how similar the various frameworks are to this one.
# components.tcl -- # Basic framework for setting up simulation systems built # from components. # # # Framework -- # Namespace to hold all procedures # namespace eval ::Framework { variable resource_data namespace export resource request supply run } # resource -- # Register a resource # # Arguments: # category Category to which it belongs # name Name of the resource # attributes Constant properties # args Command to be used to compute # the resource # # Result: # None # proc ::Framework::resource {category name attributes args} { variable resource_data lappend resource_data($category,resources) $name set resource_data($category,$name,attributes) $attributes set resource_data($category,$name,cmd) $args # TODO: check uniqueness, absence of commas # TODO: category != System # TODO: args != {} } # whichResources -- # Get all resources of a particular category # # Arguments: # category Category to query # # Result: # The list of known resources # proc ::Framework::whichResources {category} { variable resource_data return $resource_data($category,resources) # TODO: check the category exists } # attributes -- # Get the attributes of a resource # # Arguments: # category Category to which it belongs # name Name of the resource # # Result: # The list of attributes as registered # proc ::Framework::attributes {category name} { variable resource_data return $resource_data($category,$name,attributes) # TODO: check the resource exists } # supply -- # Store the value of a resource # # Arguments: # category Category to which it belongs # name Name of the resource # time Time in the simulation # data Value of the resource # # Result: # None # proc ::Framework::supply {category name time data} { variable resource_data set resource_data($category,$name,data,$time) $data # TODO: check the resource exists } # request -- # Get the value of a resource or compute it now # # Arguments: # category Category to which it belongs # name Name of the resource # # Result: # The list of attributes as registered # proc ::Framework::request {category name} { variable resource_data set time $resource_data(System,time) if { [info exists resource_data($category,$name,data,$time)] } { return $resource_data($category,$name,data,$time) } else { eval $resource_data($category,$name,cmd) $time if { [info exists resource_data($category,$name,data,$time)] } { return $resource_data($category,$name,data,$time) } else { return -code error "Resource $category/$name for time $time not available" } } } # run -- # Run the simulation over a given period of time # # Arguments: # tbegin Start time # tend Stop time # tstep Time step # resources List of resources to run: list of pairs # # Result: # None # # Side effects: # The simulation is run and the actual side effects are whatever # side effects the resource commands have # proc ::Framework::run {tbegin tend tstep resources} { variable resource_data # TODO: check tstep > 0, tbegin < tend, all resources exist set count 0 set resource_data(System,tbegin) $tbegin set resource_data(System,tend) $tend set resource_data(System,tstep) $tstep set time $tbegin while { $time < $tend-0.5*$tstep } { set time [expr {$tbegin + $count * $tstep}] set resource_data(System,time) $time foreach resource $resources { foreach {category name} $resource {break} eval $resource_data($category,$name,cmd) $time } incr count # # Clean up ... # set timeCleanup [expr {$time - 2 * $tstep}] foreach elem [array names resource_data *,data,*] { set tdata [lindex [split $elem ,] 3] if { $tdata < $timeCleanup } { unset resource_data($elem) } } } } # main -- # Get it all going - test for now # # # A minimal test ... # if {0} { proc incrTime {time} { ::Framework::supply timer next $time $time } proc printTime {time} { puts "Time is: [::Framework::request timer next]" } ::Framework::resource timer next {} incrTime ::Framework::resource timer print {} printTime parray ::Framework::resource_data ::Framework::run 0 10 1 {{timer print}} parray ::Framework::resource_data } # # A more serious test: # A room is heated to make sure the temperature is regulated # according to a simple pattern: # - From 11 pm to 6 am the ideal temperature is 10 degrees C, # - From 6 am to 11 pm the temperature should be 18 degrees. # The outside temperature varies from 5 to 11 degrees over the day # # Note: # The above procedures do not yet cope well with the _previous_ # time, so I am using a few kludges to keep track of the temperature # at various stages in the computation ... # # # Auxiliary procedures # proc timeFunction {category name expression time} { ::Framework::supply $category $name $time [expr $expression] } proc setValue {category name settings time} { set timeOfDay [expr {$time %24}] foreach {t value} $settings { if { $t <= $timeOfDay } { set newvalue $value } } ::Framework::supply $category $name $time $newvalue } proc switchOnOff {category name paramName idealName time} { set paramValue [::Framework::request {*}$paramName] set idealValue [::Framework::request {*}$idealName] if { $paramValue < $idealValue } { ::Framework::supply $category $name $time 1 } else { ::Framework::supply $category $name $time 0 } } proc controlledHeating {heatingRate controlName time} { set controlValue [::Framework::request {*}$controlName] ::Framework::supply source heating $time [expr {$controlValue * $heatingRate}] } proc prevTemp {time} { ::Framework::supply room prevTemp $time $::prevTemp } proc computeTemp {heatCapacity exchangeRate time} { set totalHeat 0.0 foreach src [::Framework::whichResources source] { set heatsrc [::Framework::request source $src] set totalHeat [expr {$totalHeat + $heatsrc}] } set prevTemp [::Framework::request room prevTemp] set outside [::Framework::request env exchangeTemp] set deltt 1.0 ;# Hack!! set newTemp [expr {$prevTemp + $deltt * ($totalHeat + $exchangeRate * ($outside - $prevTemp)) / $heatCapacity}] set ::prevTemp $newTemp ;# Hack!! ::Framework::supply room temp $time $newTemp } proc printTemp {time} { set temp [::Framework::request room temp] set outside [::Framework::request env exchangeTemp] set settemp [::Framework::request control idealTemp] set heating [::Framework::request control thermostat] puts [format "%5d %5.2f %5.2f %5.2f %s" $time $temp $outside $settemp [lindex {off on} $heating]] } # # Note: The room is heated by just one heater, but there might # be other sources and sinks in another situation. # set omega [expr {2.0*acos(-1.0)/24.0}] set initTemp 10.0 set prevTemp $initTemp set heatCapacity 2.0e5 ;# J/K for the whole room set exchangeRate [expr {6.0*3600.0}] ;# 6 W/K * 3600 seconds/hour set heatingRate [expr {300.0*3600.0}] ;# 300 W * 3600 seconds/hour ::Framework::resource source heating {} controlledHeating $heatingRate {control thermostat} ::Framework::resource env exchangeTemp {} timeFunction env exchangeTemp {8.0-3.0*cos($::omega*($time-4))} ::Framework::resource control idealTemp {} setValue control idealTemp {0 10 6 18 11 10} ::Framework::resource control thermostat {} switchOnOff control thermostat {room prevTemp} {control idealTemp} ::Framework::resource room prevTemp {} prevTemp ::Framework::resource room temp {} computeTemp $heatCapacity $exchangeRate ::Framework::resource room print {} printTemp ::Framework::supply room prevTemp 0 $initTemp ;# Hack!! ::Framework::run 0 240 1 {{room temp} {room print}}