Monitor the results of a long-running calculation

Arjen Markus (7 February 2016) Long-running calculations are sometimes needed to solve a problem. But if you have to wait for a calculation to finish after say a day of computing and then examine the results only to find out that you made a mistake in the input, you wish there was some way of monitoring the results while the calculation is going on. A technique for this is "online visualization": you adapt the program so that it produces graphical output while running. But that is not possible in all environment. Here is one such environment: you start the calculation on a computer that has no graphical facilities. Another problem: the program needs to be adapted and you may not be able to do so - graphical libraries tend to be highly system-dependent and what do you want to visualize in the first place?

The two programs below are a first experiment to deal with both problems: the first reads the output files from the computational program directly (or at least that is the idea, in the code below it simply produces numbers via a simple mathematical formula). It does so upon request from the second program. The second program presents a straightforward GUI and plots the results as it gets them from the first program. The communication between the two is via sockets. So, you can just start the first program on the same computer as the computational program and start the visualization program on another one, close that program if you have seen enough and restart to see how far your computation progressed.

Note 1: the demo is not perfect. Something odd happens when you press the Select button several times. I have not figured out yet why this happens. And the programs are demos ;).

Note 2: as the data come in as they are calculated, there is no way to scale the vertical axis properly. This has to come from the user. Hence the "scale" entry.

The server program - start this first:

# olvserver.tcl --
#     Server for the online visualisation
#


# Accept --
#     Accept a connection
#
# Arguments:
#     socket            Socket to accept the connection on
#     ip                IP address
#     args              Additional arguments (not used)
#
proc Accept {socket ip args} {
    chan configure $socket -block false

    fileevent $socket readable [list getCommand $socket]
}

# CloseServer --
#     Close down the server
#
# Arguments:
#     socket           Server socket to close
#
proc CloseServer {socket} {
    close $socket

    exit
}

# getCommand --
#     Read the comand that was sent and act on it
#
# Arguments:
#     channe;          Channel to read from/write to
#
proc getCommand {channel} {
    global line

    if { ! [eof $channel] } {
        gets $channel line

        switch -- [lindex $line 0] {
            "GETTIME" {
                # Send start and stop time
                puts $channel "TIMESTART 0"
                puts $channel "TIMESTOP 1000"
                flush $channel
            }
            "GETPARAMETERS" {
                # Send the names of the parameters/substances
                foreach p {A B C} {
                    puts $channel [list PARAMETERNAME $p]
                }
                flush $channel
            }
            "GETLOCATIONS" {
                # Send the names of the parameters/substances
                foreach l {loc1 loc2 loc3 loc4} {
                    puts $channel [list LOCATIONNAME $l]
                }
                flush $channel
            }
            "GETVALUE" {
                set record [lindex $line 1]
                if { $record >= 0 && $record <= 50 } {
                    set t [expr {20.0 * $record}]
                    puts $channel "VALUE $t [expr {sin($t/100.0)}]"
                } else {
                    puts $channel "NOVALUE"
                }
                flush $channel
            }
            "DONEINITIAL" {
                puts $channel "DONEINITIAL"
                flush $channel
            }
            "RETRIEVEVALUES" {
                lassign $line keyword substanceIdx locationIdx
            }
        }
    } else {
        close $channel
    }
}

# main --
#     Start the server - for debugging/development purposes:
#     make sure that the socket is closed.
#
set port   8081
set socket [socket -server [list Accept] $port]

wm protocol . WM_DELETE_WINDOW [list CloseServer $socket]

pack [label .label -textvariable line -width 80]

The client program:

# onlinevis.tcl --
#     Online visualisation and inspection for DELWAQ
#
package require Plotchart

# SetupWindow --
#     Set up the main window
#
# global variables
#
global substance      ;# Chosen substance
global substanceList  ;# List of substances to choose from
global location       ;# Chosen location
global locationList   ;# List of locations to choose from
global scale          ;# Scale for the vertical axis

global status         ;# Text showing what the program is doing

global plotParameters ;# Dictionary containing plot parameters
#
# Arguments:
#     None
#
proc SetupWindow {} {
    global substance
    global substanceList
    global location
    global locationList
    global status
    global scale
    #
    # Simple menu
    #
    menu .menu
    menu .menu.file -tearoff false
    .menu add cascade -label File -menu .menu.file
    . configure -menu .menu

    .menu.file add command -label Exit -underline 1 -command exit

    #
    # Dropdown buttons for the substance and the location
    #
    ttk::label    .textSubstance   -text "Parameter:"
    ttk::label    .textLocation    -text "Location:"
    ttk::label    .textScale       -text "Scale:"
    ttk::combobox .selectSubstance -values $substanceList -textvariable substance
    ttk::combobox .selectLocation  -values $locationList  -textvariable location
    ttk::entry    .editScale       -textvariable scale
    ttk::button   .getData         -text "Select" -command [list getNewData]

    #
    # Create the canvas window
    #
    canvas .cnv -width 800 -height 500 -bg white

    #
    # Status bar for messages
    #
    ttk::label .status -textvariable status -relief sunken

    #
    # Put them in the main window
    #
    grid .textSubstance .selectSubstance .textLocation .selectLocation .textScale .editScale .getData -padx 4 -pady 4 -sticky w
    grid .cnv           -                -             -               -          -          -        -sticky news
    grid .status        -                -             -               -          -          -        -sticky news
}

# ConnectHost --
#     Connect to the server (list to port 8081)
#
# Arguments:
#     host           Name of the host
#
proc ConnectHost {host} {
    global channel
    global status

    set channel [socket $host 8081]

    #after 10000 [list BreakConnection $channel]

    fileevent $channel readable [list getData $channel]

    set status "Retrieving parameter and location names ..."
    SendInitialCommands $channel
}

# SendInitialCommands --
#     Send the initial commands to the server
#
# Arguments:
#     channel        Channel to the server
#
proc SendInitialCommands {channel} {
    global status
    global plot
    global plotParameters

    puts $channel "GETPARAMETERS"
    puts $channel "GETLOCATIONS"
    puts $channel "GETTIME"
    puts $channel "DONEINITIAL"
    flush $channel

    set status "Ready"
}

# PlotData --
#     PLot the data just received
#
# Arguments:
#     time           Value for x-axis
#     value          Value for y-axis
#
proc PlotData {time value} {
    global plot
    global scale
    global plotParameters

    if { $plot == "" } {
        set tstart [dict get $plotParameters timestart]
        set tstop  [dict get $plotParameters timestop]

        set plot [::Plotchart::createXYPlot .cnv [::Plotchart::determineScale $tstart $tstop] \
                                                 [::Plotchart::determineScale 0.0 $scale]]
    }

    $plot plot data $time $value
}

# getData --
#     Get data from the server
#
# Arguments:
#     channel           Channel to the server
#
proc getData {channel} {
    global plotParameters
    global substanceList
    global locationList
    global substance
    global location
    global record
    global status

    if { ! [eof $channel] } {
        gets $channel line
        #puts $line

        switch -- [lindex $line 0] {
            "PARAMETERNAME" {
                lappend substanceList [lindex $line 1]
            }
            "LOCATIONNAME" {
                lappend locationList [lindex $line 1]
            }
            "TIMESTART" {
                dict set plotParameters timestart [lindex $line 1]
            }
            "TIMESTOP" {
                dict set plotParameters timestop [lindex $line 1]
            }
            "VALUE" {
                puts $line
                PlotData [lindex $line 1] [lindex $line 2]
                after 10 [list getNextValue $channel [incr record]]
            }
            "NOVALUE" {
                # We must wait
                set status "Waiting ..."
                after 100 [list getNextValue $channel $record]
            }
            "DONEINITIAL" {
                .selectSubstance configure -values $substanceList
                .selectLocation  configure -values $locationList
                set substance [lindex $substanceList 0]
                set location  [lindex $locationList 0]
            }
        }
    } else {
        close $channel
    }
}

# getNextValue --
#     Send the command to retrieve the next value
#
# Arguments:
#     channel             The channel to the server
#     record              The index of the record to retrieve
#
proc getNextValue {channel record} {
    puts $channel "GETVALUE $record"
    flush $channel
}

# getNewData --
#     Get the data that are available for the selected substance and location
#
# Arguments:
#     None
#
proc getNewData {} {
    global status
    global channel
    global substance
    global location
    global substanceList
    global locationList
    global record
    global plot

    set record 0

    set substanceIdx [lsearch $substanceList $substance]
    set locationIdx  [lsearch $locationList  $location]

    puts $channel "RETRIEVEVALUES $substanceIdx $locationIdx"
    puts $channel "GETVALUE $record"
    flush $channel

    if { $plot != "" } {
        .cnv delete all
        $plot deletedata
        set plot ""
    }

    set status "Retrieving data ..."
}

# main --
#     Start the program
#
if { [llength $::argv] == 0 } {
    tk_messageBox -type ok -message "Usage: onlinevis <name of host>"
    exit
}

set plot           {}
set plotParameters {}
set substanceList  {}
set substance      {}
set locationList   {}
set location       {}
set scale          1.0

set status         "Waiting for connection ..."

SetupWindow

ConnectHost [lindex $argv 0]