source: trunk/gui/scripts/resultset.tcl @ 3093

Last change on this file since 3093 was 3074, checked in by mmc, 12 years ago

Fixed a problem with the <number> results. The simulation number (#1, etc.)
wasn't being reported with a normalized value, so it didn't know where to
plot them along the x axis. Also, values like 77K did have a normalized
value, but it was in the wrong place (index 0 instead of index 1).

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