Simulation framework

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:

  • The room itself is one component and for its functioning it depends on the outside temperature and the heating (as can be seen from the "requests" in the body of the procedure computeTemp).
  • The heating is a component that depends on the thermostat.
  • The thermostat requires the temperature from the previous time and the set temperature for its decision to turn the heater on or off.
  • As the room is the dependent component, it is used in the "run" procedure to drive the simulation (though the print component "printTemp" would have served just as well).

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}}