source: trunk/gui/scripts/resultset.tcl

Last change on this file was 6025, checked in by ldelgass, 8 years ago

whitespace

File size: 20.8 KB
Line 
1# -*- mode: tcl; indent-tabs-mode: nil -*-
2# ----------------------------------------------------------------------
3#  COMPONENT: ResultSet - set of XML objects for simulated results
4#
5#  This data structure collects all of the simulated results
6#  produced by a series of tool runs.  It is used by the Analyzer,
7#  ResultSelector, and other widgets to keep track of all known runs
8#  and visualize the result that is currently selected.  Each run
9#  has an index number ("#1", "#2", "#3", etc.) that can be used to
10#  label the run and refer to it later.
11# ======================================================================
12#  AUTHOR:  Michael McLennan, Purdue University
13#  Copyright (c) 2004-2012  HUBzero Foundation, LLC
14#
15#  See the file "license.terms" for information on usage and
16#  redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES.
17# ======================================================================
18package require Itcl
19
20itcl::class Rappture::ResultSet {
21    constructor {args} { # defined below }
22    destructor { # defined below }
23
24    public method add {xmlobj}
25    public method clear {{xmlobj ""}}
26    public method diff {option args}
27    public method find {collist vallist}
28    public method get {collist xmlobj}
29    public method contains {xmlobj}
30    public method size {}
31
32    public method notify {option args}
33    protected method _notifyHandler {args}
34
35    protected method _addOneResult {tuples xmlobj {simnum ""}}
36
37    private variable _dispatcher ""  ;# dispatchers for !events
38    private variable _results ""     ;# tuple of known results
39    private variable _resultnum 0    ;# counter for result #1, #2, etc.
40    private variable _notify         ;# info used for notify command
41}
42
43# ----------------------------------------------------------------------
44# CONSTRUCTOR
45# ----------------------------------------------------------------------
46itcl::body Rappture::ResultSet::constructor {args} {
47    # create a dispatcher for events
48    Rappture::dispatcher _dispatcher
49    $_dispatcher register !change
50    $_dispatcher dispatch $this !change \
51        [itcl::code $this _notifyHandler]
52
53    # create a list of tuples for data
54    set _results [Rappture::Tuples ::#auto]
55    $_results column insert end -name xmlobj -label "top-level XML object"
56    $_results column insert end -name simnum -label "simulation number"
57
58    # clear notification info
59    set _notify(ALL) ""
60
61    eval configure $args
62}
63
64# ----------------------------------------------------------------------
65# DESTRUCTOR
66# ----------------------------------------------------------------------
67itcl::body Rappture::ResultSet::destructor {} {
68    clear
69    itcl::delete object $_results
70}
71
72# ----------------------------------------------------------------------
73# USAGE: add <xmlobj>
74#
75# Adds a new result to this result set.  Scans through all existing
76# results to look for a difference compared to previous results.
77# Returns the simulation number (#1, #2, #3, etc.) of this new result
78# to the caller.  The various data objects for this result set should
79# be added to their result viewers at the same index.
80# ----------------------------------------------------------------------
81itcl::body Rappture::ResultSet::add {xmlobj} {
82    set xmlobj0 [$_results get -format xmlobj end]
83    if {$xmlobj0 eq ""} {
84        #
85        # If this is the first result, then there are no diffs.
86        # Add it right in.
87        #
88        set simnum "#[incr _resultnum]"
89        $_results insert end [list $xmlobj $simnum]
90    } else {
91        #
92        # For all later results, find the diffs and add any new columns
93        # into the results tuple.  The latest result is the most recent.
94        #
95        set simnum [_addOneResult $_results $xmlobj]
96    }
97
98    # make sure we fix up associated controls
99    $_dispatcher event -now !change op add what $xmlobj
100
101    return $simnum
102}
103
104# ----------------------------------------------------------------------
105# USAGE: clear ?<xmlobj>?
106#
107# Clears one or all results in this result set.  If no specific
108# result object is specified, then all results are cleared.
109# ----------------------------------------------------------------------
110itcl::body Rappture::ResultSet::clear {{xmlobj ""}} {
111    if {$xmlobj ne ""} {
112        #
113        # Delete just one result.  Look for the result among the
114        # tuples and remove it.  Then, rebuild all of the tuples
115        # by scanning back through them and building them back up.
116        # This will rebuild the columns/controls as they should
117        # be now, removing anything that is no longer necessary.
118        #
119        set irun [$_results find -format xmlobj $xmlobj]
120        if {[llength $irun] == 1} {
121            # grab a description of what we're about to delete
122            set dlist [list simnum [$_results get -format simnum $irun]]
123            foreach col [lrange [$_results column names] 2 end] {
124                set raw [lindex [Rappture::LibraryObj::value $xmlobj $col] 0]
125                lappend dlist $col $raw  ;# use "raw" (user-readable) label
126            }
127
128            # delete this from the tuples of all results
129            itcl::delete object $xmlobj
130            $_results delete $irun
131
132            set new [Rappture::Tuples ::#auto]
133            $new column insert end -name xmlobj -label "top-level XML object"
134            $new column insert end -name simnum -label "simulation number"
135
136            for {set n 0} {$n < [$_results size]} {incr n} {
137                set rec [lindex [$_results get -format {xmlobj simnum} $n] 0]
138                foreach {obj num} $rec break
139                if {$n == 0} {
140                    $new insert end [list $obj $num]
141                } else {
142                    _addOneResult $new $obj $num
143                }
144            }
145
146            # plug in the new set of rebuilt tuples
147            itcl::delete object $_results
148            set _results $new
149
150            # make sure we fix up associated controls at some point
151            $_dispatcher event -now !change op clear what $dlist
152        }
153    } else {
154        #
155        # Delete all results.
156        #
157        for {set irun 0} {$irun < [$_results size]} {incr irun} {
158            set xo [$_results get -format xmlobj $irun]
159            itcl::delete object $xo
160        }
161        $_results delete 0 end
162
163        # make sure we fix up associated controls at some point
164        $_dispatcher event -now !change op clear what all
165    }
166
167    if {[$_results size] == 0} {
168        # no results left?  then reset to a clean state
169        eval $_results column delete [lrange [$_results column names] 2 end]
170        set _resultnum 0
171    }
172}
173
174# ----------------------------------------------------------------------
175# USAGE: diff names
176# USAGE: diff values <column> ?<which>?
177#
178# Returns information about the diffs in the current set of known
179# results.  The "diff names" returns a list of column names for
180# parameters that have diffs.  (These are the columns in the tuples.)
181#
182# The "diff values" returns the various values associated with a
183# particular <column> name.  If the optional <which> is specified,
184# then it is treated as an index into the list of values--0 for the
185# first value, 1 for the second, etc.  Each value is returned as
186# a list with two words.  The first is the the label associated with
187# the value.  The second is the normalized (numeric) value, which can
188# be sorted to get a particular ordering.
189# ----------------------------------------------------------------------
190itcl::body Rappture::ResultSet::diff {option args} {
191    switch -- $option {
192        names {
193            return [$_results column names]
194        }
195        values {
196            if {[llength $args] < 1} {
197                error "wrong # args: should be \"diff values col ?which?\""
198            }
199            set col [lindex $args 0]
200
201            set which "all"
202            if {[llength $args] > 1} {
203                set which [lindex $args 1]
204            }
205
206            set rlist ""
207            # build an array of normalized values and their labels
208            if {$col == "simnum"} {
209                set nruns [$_results size]
210                for {set n 0} {$n < $nruns} {incr n} {
211                    set simnum [$_results get -format simnum $n]
212                    lappend rlist $simnum $n
213                }
214            } else {
215                set havenums 1
216                foreach rec [$_results get -format [list xmlobj $col]] {
217                    set xo [lindex $rec 0]
218                    set v [lindex $rec 1]
219                    foreach {raw norm} \
220                        [Rappture::LibraryObj::value $xo $col] break
221                    if {![info exists unique($v)]} {
222                        # keep only unique label strings
223                        set unique($v) $norm
224                    }
225                    if {$havenums && ![string is double $norm]} {
226                        set havenums 0
227                    }
228                }
229
230                if {!$havenums} {
231                    # don't have normalized nums? then sort and create nums
232                    set rlist ""
233                    set n 0
234                    foreach val [lsort -dictionary [array names unique]] {
235                        lappend rlist $val [incr n]
236                    }
237                } else {
238                    set rlist [array get unique]
239                }
240            }
241
242            if {$which eq "all"} {
243                return $rlist
244            }
245
246            # treat the "which" parameter as an XML object
247            set irun [lindex [$_results find -format xmlobj $which] 0]
248            if {$irun ne ""} {
249                set val [lindex [$_results get -format $col $irun] 0]
250                array set val2norm $rlist
251                if {[info exists val2norm($val)]} {
252                    return [list $val $val2norm($val)]
253                }
254            }
255        }
256        default {
257            error "bad option \"$option\": should be names or values"
258        }
259    }
260}
261
262# ----------------------------------------------------------------------
263# USAGE: find <columnList> <valueList>
264#
265# Searches through the results for a set of tuple values that match
266# the <valueList> for the given <columnList>.  Returns a list of
267# matching xml objects or "" if there is no match.  If the <valueList>
268# is *, then it returns a list of all xml objects.
269# ----------------------------------------------------------------------
270itcl::body Rappture::ResultSet::find {collist vallist} {
271    if {$vallist eq "*"} {
272        return [$_results get -format xmlobj]
273    }
274
275    set rlist ""
276    foreach irun [$_results find -format $collist -- $vallist] {
277        lappend rlist [$_results get -format xmlobj $irun]
278    }
279    return $rlist
280}
281
282# ----------------------------------------------------------------------
283# USAGE: get <columnList> <xmlobj>
284#
285# Returns values for the specified <columnList> for the given <xmlobj>.
286# This is a way of querying associated data for the given object.
287# ----------------------------------------------------------------------
288itcl::body Rappture::ResultSet::get {collist xmlobj} {
289    set irun [lindex [$_results find -format xmlobj $xmlobj] 0]
290    if {$irun ne ""} {
291        return [lindex [$_results get -format $collist $irun] 0]
292    }
293    return ""
294}
295
296# ----------------------------------------------------------------------
297# USAGE: contains <xmlobj>
298#
299# Checks to see if the given <xmlobj> is already represented by
300# some result in this result set.  This comes in handy when checking
301# to see if an input case is already covered.
302#
303# Returns 1 if the result set already contains this result, and
304# 0 otherwise.
305# ----------------------------------------------------------------------
306itcl::body Rappture::ResultSet::contains {xmlobj} {
307    # no results? then this must be new
308    if {[$_results size] == 0} {
309        return 0
310    }
311
312    #
313    # Compare this new object against the last XML object in the
314    # results set.  If it has a difference, make sure that there
315    # is a column to represent the quantity with the difference.
316    #
317    set xmlobj0 [$_results get -format xmlobj end]
318    foreach {op vpath oldval newval} [$xmlobj0 diff $xmlobj] {
319        if {[$xmlobj get $vpath.about.diffs] == "ignore"} {
320            continue
321        }
322        if {$op == "+" || $op == "-"} {
323            # ignore differences where parameters come and go
324            # such differences make it hard to work controls
325            continue
326        }
327        if {[$_results column names $vpath] == ""} {
328            # no column for this quantity yet
329            return 0
330        }
331    }
332
333    #
334    # If we got this far, then look through existing results for
335    # matching tuples, then check each one for diffs.
336    #
337    set format ""
338    set tuple ""
339    foreach col [lrange [$_results column names] 2 end] {
340        lappend format $col
341        set raw [lindex [Rappture::LibraryObj::value $xmlobj $col] 0]
342        lappend tuple $raw  ;# use the "raw" (user-readable) label
343    }
344    if {[llength $format] > 0} {
345        set ilist [$_results find -format $format -- $tuple]
346    } else {
347        set ilist 0  ;# no diffs -- must match first entry
348    }
349
350    foreach i $ilist {
351        set xmlobj0 [$_results get -format xmlobj $i]
352        set diffs [$xmlobj0 diff $xmlobj]
353        if {[llength $diffs] == 0} {
354            # no diffs -- already contained here
355            return 1
356        }
357    }
358
359    # must be some differences
360    return 0
361}
362
363
364# ----------------------------------------------------------------------
365# USAGE: size
366#
367# Returns the number of results currently stored in the set.
368# ----------------------------------------------------------------------
369itcl::body Rappture::ResultSet::size {} {
370    return [$_results size]
371}
372
373# ----------------------------------------------------------------------
374# USAGE: notify add <client> ?!event !event ...? <command>
375# USAGE: notify get ?<client>? ?!event?
376# USAGE: notify remove <client> ?!event !event ...?
377#
378# Clients use this to add/remove requests for notifications about
379# various events that signal changes to the data in each ResultSet.
380#
381# The "notify add" operation takes a <client> name (any unique string
382# identifying the client), an optional list of events, and the <command>
383# that should be called for the callback.
384#
385# The "notify get" command returns information about clients and their
386# registered callbacks.  With no args, it returns a list of <client>
387# names.  If the <client> is specified, it returns a list of !events.
388# If the <client> and !event is specified, it returns the <command>.
389#
390# The "notify remove" command removes any callback associated with
391# a given <client>.  If no particular !events are specified, then it
392# removes callbacks for all events.
393# ----------------------------------------------------------------------
394itcl::body Rappture::ResultSet::notify {option args} {
395    set allEvents {!change}
396    switch -- $option {
397        add {
398            if {[llength $args] < 2} {
399                error "wrong # args: should be \"notify add caller ?!event !event ...? command"
400            }
401            set caller [lindex $args 0]
402            set command [lindex $args end]
403            if {[llength $args] > 2} {
404                set events [lrange $args 1 end-1]
405            } else {
406                set events $allEvents
407            }
408
409            foreach name $events {
410                if {[lsearch -exact $allEvents $name] < 0} {
411                    error "bad event \"$name\": should be [join $allEvents ,]"
412                }
413                if {[lsearch $_notify(ALL) $caller] < 0} {
414                    lappend _notify(ALL) $caller
415                }
416                set _notify($caller-$name) $command
417            }
418        }
419        get {
420            switch -- [llength $args] {
421                0 {
422                    return $_notify(ALL)
423                }
424                1 {
425                    set caller [lindex $args 0]
426                    set rlist ""
427                    foreach key [array names _notify $caller-*] {
428                        lappend rlist [lindex [split $key -] end]
429                    }
430                    return $rlist
431                }
432                2 {
433                    set caller [lindex $args 0]
434                    set name [lindex $args 1]
435                    if {[info exists _notify($caller-$name)]} {
436                        return $_notify($caller-$name)
437                    }
438                    return ""
439                }
440                default {
441                    error "wrong # args: should be \"notify get ?caller? ?!event?\""
442                }
443            }
444        }
445        remove {
446            if {[llength $args] < 1} {
447                error "wrong # args: should be \"notify remove caller ?!event !event ...?"
448            }
449            set caller [lindex $args 0]
450            if {[llength $args] > 1} {
451                set events [lrange $args 1 end]
452            } else {
453                set events $allEvents
454            }
455
456            foreach name $events {
457                catch {unset _notify($caller-$name)}
458            }
459            if {[llength [array names _notify $caller-*]] == 0} {
460                set i [lsearch $_notify(ALL) $caller]
461                if {$i >= 0} {
462                    set _notify(ALL) [lreplace $_notify(ALL) $i $i]
463                }
464            }
465        }
466        default {
467            error "wrong # args: should be add, get, remove"
468        }
469    }
470}
471
472# ----------------------------------------------------------------------
473# USAGE: _notifyHandler ?<eventArgs>...?
474#
475# Called automatically whenever a !change event is triggered in this
476# object.  Scans through the list of clients that want to receive this
477# event and executes each of their callbacks.
478# ----------------------------------------------------------------------
479itcl::body Rappture::ResultSet::_notifyHandler {args} {
480    array set data $args
481    set event $data(event)
482
483    foreach caller $_notify(ALL) {
484        if {[info exists _notify($caller-$event)]} {
485            if {[catch {uplevel #0 $_notify($caller-$event) $args} result]} {
486                # anything go wrong? then throw a background error
487                bgerror "$result\n(while dispatching $event to $caller)"
488            }
489        }
490    }
491}
492
493# ----------------------------------------------------------------------
494# USAGE: _addOneResult <tuples> <xmlobj> ?<simNum>?
495#
496# Used internally to add one new <xmlobj> to the given <tuples>
497# object.  If the new xmlobj contains different input parameters
498# that are not already columns in the tuple, then this routine
499# creates the new columns.  If the optional <simNum> is specified,
500# then it is added as the simulation number #1, #2, #3, etc.  If
501# not, then the new object is automatically numbered.
502# ----------------------------------------------------------------------
503itcl::body Rappture::ResultSet::_addOneResult {tuples xmlobj {simnum ""}} {
504    #
505    # Compare this new object against the last XML object in the
506    # results set.  If it has a difference, make sure that there
507    # is a column to represent the quantity with the difference.
508    #
509    set xmlobj0 [$tuples get -format xmlobj end]
510    foreach {op vpath oldval newval} [$xmlobj0 diff $xmlobj] {
511        if {[$xmlobj get $vpath.about.diffs] == "ignore"} {
512            continue
513        }
514        if {$op == "+" || $op == "-"} {
515            # ignore differences where parameters come and go
516            # such differences make it hard to work controls
517            continue
518        }
519
520        # make sure that these values really are different
521        set oldval [lindex [Rappture::LibraryObj::value $xmlobj0 $vpath] 0]
522        set newval [lindex [Rappture::LibraryObj::value $xmlobj $vpath] 0]
523
524        if {$oldval != $newval && [$tuples column names $vpath] == ""} {
525            # no column for this quantity yet
526            $tuples column insert end -name $vpath -default $oldval
527        }
528    }
529
530    # build a tuple for this new object
531    set cols ""
532    set tuple ""
533    foreach col [lrange [$tuples column names] 2 end] {
534        lappend cols $col
535        set raw [lindex [Rappture::LibraryObj::value $xmlobj $col] 0]
536        lappend tuple $raw  ;# use the "raw" (user-readable) label
537    }
538
539    # find a matching tuple? then replace it -- only need one
540    if {[llength $cols] > 0} {
541        set ilist [$tuples find -format $cols -- $tuple]
542    } else {
543        set ilist 0  ;# no diffs -- must match first entry
544    }
545
546    # add all remaining columns for this new entry
547    set tuple [linsert $tuple 0 $xmlobj]
548    set cols [linsert $cols 0 "xmlobj"]
549
550    if {[llength $ilist] > 0} {
551        if {[llength $ilist] > 1} {
552            error "why so many matching results?"
553        }
554
555        # overwrite the first matching entry
556        # start by freeing the old result
557        set index [lindex $ilist 0]
558        set xo [$tuples get -format xmlobj $index]
559        itcl::delete object $xo
560
561        # put this new result in its place
562        $tuples put -format $cols $index $tuple
563        set simnum [$tuples get -format simnum $index]
564    } else {
565        if {$simnum eq ""} {
566            set simnum "#[incr _resultnum]"
567        }
568        set tuple [linsert $tuple 1 $simnum]
569        $tuples insert end $tuple
570    }
571    return $simnum
572}
Note: See TracBrowser for help on using the repository browser.