1 import string
2 from Tkinter import *
3 import tkMessageBox
4 import Pmw
5 import tkFileDialog
6 from xml.dom.minidom import *
7
8 from teamwork.messages.PsychMessage import Message as PsychMessage
9 from teamwork.widgets.MultiWin import InnerWindow
10 from teamwork.widgets.TreeWidget import *
11 from teamwork.agent.Agent import Agent
12 from teamwork.action.PsychActions import Action
13 from teamwork.math.probability import Distribution
14 from teamwork.math.matrices import epsilon
15 from teamwork.math.Keys import Key
16 from teamwork.math.KeyedVector import KeyedVector,UnchangedRow
17
18 try:
19 from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
20 from reportlab.rl_config import defaultPageSize
21 from reportlab.lib.styles import getSampleStyleSheet
22 from reportlab.lib.units import inch
23 pdfCapability = True
24 except ImportError:
25 pdfCapability = False
26
27
29 """
30 @ivar history: step history (allows collapsing of redundant nodes)
31 @type history: dict
32 @ivar doc: cumulative XML history (includes all steps, even those that are not displayed due to redundancy)
33 @type doc: Element
34 """
35
37 optiondefs = (
38 ('title', 'AAR', self.setTitle),
39 ('font', ("Helvetica", 10, "normal"), Pmw.INITOPT),
40 ('fitCommand', None, Pmw.INITOPT),
41 ('msgCommand', None, Pmw.INITOPT),
42 ('reportTitle', "Scenario History Report", None),
43 ('pageinfo', "history report", None),
44 ('expert', False, None),
45 ('entities',{},None),
46 )
47 self.defineoptions(kw,optiondefs)
48 InnerWindow.__init__(self,frame)
49 Button(self.component('frame'), text="Generate report",
50 command=self.generateReport).pack(side='top')
51 widget = self.createcomponent('Tree',(),None,EasyTree,
52 (self.component('frame'),),
53 root_label="Simulation Trace",
54 font=self['font'],
55 )
56
57 sb=Scrollbar(widget.master)
58 sb.pack(side=RIGHT, fill=Y)
59 widget.configure(yscrollcommand=sb.set)
60 sb.configure(command=widget.yview)
61
62 widget.pack(side='top',fill='both',expand='yes')
63 self.initialiseoptions()
64 self.history = {}
65 self.clear()
66
68 if not self['entities']:
69 tkMessageBox.showerror('Report Error','There are no agents on which to report.')
70 return
71 widget = self.component('Tree')
72 paragraphs = []
73 self.getNodeText(widget.easyRoot, 0, paragraphs)
74 ftypes = [('XML file', '.xml')]
75 if pdfCapability:
76 ftypes.append(('PDF file', '.pdf'))
77 filename = tkFileDialog.asksaveasfilename(filetypes=ftypes,
78 defaultextension='.xml')
79 if filename:
80 if filename[-4:] == '.pdf':
81
82 doc = SimpleDocTemplate(filename)
83 Story = [Spacer(1, 2*inch)]
84 style = getSampleStyleSheet()["Normal"]
85 for (para,level) in paragraphs:
86 indent = level*8
87 para = "<para firstLineIndent=\"0\" leftIndent=\"" + str(indent) + "\">" + para + "</para>"
88 p = Paragraph(para, style)
89 Story.append(p)
90 Story.append(Spacer(1, 0.2*inch))
91 doc.build(Story, onFirstPage=self.myFirstPage, onLaterPages=self.myLaterPages)
92 elif filename[-4:] == '.xml':
93
94 f = open(filename,'w')
95 f.write(self.doc.toxml())
96 f.close()
97 else:
98 raise NotImplementedError,'Unable to output history in %s format' % (filename[-3:].upper())
99
100 - def myFirstPage(self, canvas, doc):
101 PAGE_WIDTH = defaultPageSize[0]
102 PAGE_HEIGHT = defaultPageSize[1]
103 canvas.saveState()
104 canvas.setFont('Times-Bold', 16)
105 canvas.drawCentredString(PAGE_WIDTH/2.0, PAGE_HEIGHT-108,
106 self['reportTitle'])
107 canvas.setFont('Times-Roman', 9)
108 canvas.drawString(inch, 0.75 * inch,
109 "First Page / %s" % self['pageinfo'])
110 canvas.restoreState()
111
112 - def myLaterPages(canvas, doc):
113 canvas.saveState()
114 canvas.setFont('Times-Roman', 9)
115 canvas.drawString(inch, 0.75 * inch,
116 "Page %d %s" % (doc.page, self['pageinfo']))
117 canvas.restoreState()
118
119 - def getNodeText(self, node, level, paragraphs):
120 paragraphs.append((node['label'], level))
121 for child in node['children']:
122 self.getNodeText(child, level+1, paragraphs)
123
125 """Removes any existing explanations from the display
126 """
127 widget = self.component('Tree')
128 self.nodes = []
129 rootNode = widget.easyRoot
130 rootNode.deleteChildren()
131
132
133 self.doc = Document()
134 root = self.doc.createElement('history')
135 self.doc.appendChild(root)
136
137 self.history.clear()
138
140 widget = self.component('Tree')
141 widget.inhibitDraw = True
142 step = elements.firstChild
143 while step:
144 self.doc.documentElement.appendChild(step)
145 if step.tagName == 'step':
146 parent = self.addStep(step)
147
148 widget.root.expand()
149 else:
150 raise UserWarning,step.tagName
151 step = step.nextSibling
152 widget.inhibitDraw = False
153
155 widget = self.component('Tree')
156 index = int(step.getAttribute('time'))
157 prefix = 'Step %d' % (index)
158
159 parentStepNode = self.findNode(prefix,widget.easyRoot)
160 if str(step.getAttribute('hypothetical')) == str(True):
161 prefix += ' (hypothetical)'
162 parentStepNode['label'] = prefix
163
164 field = step.firstChild
165 decisionStr = []
166 actors = {}
167 while field:
168 if field.tagName == 'turn':
169
170 child = field.firstChild
171 while child:
172 if child.tagName == 'decision':
173
174 name = str(field.getAttribute('agent'))
175 actors[name] = True
176 turnStr = '%s ' % (name)
177 label = '%s\'s decision' % (name)
178 forced = str(field.getAttribute('forced')) == str(True)
179 node = self.findNode(label,parentStepNode)
180 option = extractAction(child)
181 actionStr = actionString(option)
182 decisionStr.append(turnStr+actionStr)
183 if forced:
184 node['label'] = label + ' (forced)'
185 node['isLeaf'] = True
186 else:
187 node['action'] = lambda n,e,s=self,a=name,o=option,\
188 t=index:s.confirmAction(n,e,a,o,t)
189 elif child.tagName == 'explanation':
190 self.addExplanation(name,child,node,index)
191 elif child.tagName == 'suggestions':
192 self.addSuggestions(name,child,node)
193 child = child.nextSibling
194 elif field.tagName == 'effect':
195 self.addEffect(field,parentStepNode)
196 else:
197 print 'Unhandled:',field.tagName
198 field = field.nextSibling
199
200 names = actors.keys()
201 names.sort()
202 key = '%d %s' % (index,' '.join(names))
203 try:
204 index = widget.easyRoot.childIndex(self.history[key])
205 except KeyError:
206 index = -1
207 if index == -1:
208 self.history[key] = parentStepNode
209
210
211
212 parentStepNode['label'] = prefix + ': ' + '; '.join(decisionStr)
213 parentStepNode.refresh(widget)
214 return parentStepNode
215
217 """
218 Activates the fitting function for the given agent and alternative
219 @param agent: the agent whose behavior is being fit
220 @type agent: str
221 @param element: the explanation structure for this alternative action
222 @type element: Element
223 @param step: the time that this action should occur
224 @type step: int
225 """
226 option = []
227 child = element.firstChild
228 while child:
229 if child.tagName == 'action':
230 action = Action()
231 action.parse(child)
232 option.append(action)
233 child = child.nextSibling
234 self['fitCommand'](agent,option,step-1)
235
237 menu = Menu(self.component('frame'),tearoff=0)
238 label = string.join(map(str,args['decision']),', ')
239 menu.add_command(label='%s is correct' % (label),
240 command=lambda s=self,a=args['decision'],i=args:\
241 s['fitCommand'](a,i))
242 if len(args['agent'].actions.getOptions()) > 1:
243 menu.add_separator()
244 for action in args['agent'].actions.getOptions():
245 if action != args['decision']:
246 menu.add_command(label=string.join(map(str,action),', '),
247 command=lambda s=self,a=action,i=args:\
248 s['fitCommand'](a,i))
249 menu.bind("<Leave>",self.unpost)
250 menu.post(event.x_root, event.y_root)
251
253 menu = Menu(self.component('frame'),tearoff=0)
254 agent = self['entities'][args['violator']]
255 entities = filter(lambda e: e.name != args['violator'] and \
256 len(e.entities) > 0,agent.getEntityBeliefs())
257 for entity in entities:
258 menu.add_command(label='Send from %s' % \
259 (entity.name),
260 command=lambda o=args,e=entity.name,s=self: \
261 s['msgCommand'](e,o))
262 menu.bind("<Leave>",self.unpost)
263 menu.post(event.x_root, event.y_root)
264
266 """Removes the bottom-most bullet from the tree"""
267 widget = self.component('Tree')
268 del widget.child[0].child[-1]
269
270 - def unpost(self,event):
271 event.widget.unpost()
272
274 """
275 @param label: the label prefix for the child node
276 @type label: str
277 @param parent: the parent node
278 @return: the child node for the given parent with the given label. If no such child already exists, then returns a newly created child node with the given label
279 """
280 widget = self.component('Tree')
281 n = None
282 for node in parent['children']:
283 if node['label'][:len(label)] == label:
284 n = node
285 break
286
287 if not n:
288 n = parent.addChild(widget,label=label)
289 n['label'] = label
290
291 return n
292
294 """Extracts an agent's decision from an XML element and adds the corresponding subtree to the given parent tree
295 @param agent: the agent whose decision is being explained
296 @type agent: str
297 @param element: the XML decision description
298 @type element: Element
299 @param parent: the node to add the decision explanation below
300 @type parent: L{Node}
301 @param step: the time that this decision occurred at
302 @type step: int
303 """
304 widget = self.component('Tree')
305 child = element.firstChild
306 while child:
307 if child.tagName == 'alternatives':
308 subNode = self.findNode('Alternatives:',parent)
309 subNode.deleteChildren()
310 alternatives = []
311 subChild = child.firstChild
312 while subChild:
313 if subChild.tagName == 'goals':
314 goals = KeyedVector()
315 goals.parse(subChild.firstChild,True)
316 else:
317 alternatives.append(subChild)
318 subChild = subChild.nextSibling
319 for subChild in alternatives:
320
321 option = extractAction(subChild)
322 label = actionString(option)
323 try:
324 rank = int(subChild.getAttribute('rank'))
325 label += ' (rank #%d)' % (rank+1)
326 except ValueError:
327 pass
328 actNode = subNode.addChild(widget,label=label)
329 actNode['action'] = lambda l,e,s=self,a=agent,o=subChild,\
330 t=step:s.fitAction(a,o,t)
331 field = subChild.firstChild
332 while field:
333 if field.tagName == 'vector':
334
335 good = []
336 bad = []
337 delta = KeyedVector()
338 delta.parse(field,True)
339 for key in goals.keys():
340 value = goals[key]*delta[key]
341 if value > epsilon:
342 good.append(key)
343 elif value < -epsilon:
344 bad.append(key)
345 if len(good) > 0:
346 self.addGoals('which would help:',actNode,
347 good,goals)
348 if len(bad) > 0:
349 self.addGoals('but which would not:',
350 actNode,bad,goals)
351 if len(good)+len(bad) == 0:
352 expNode = actNode.addChild(widget,label='which was just as good',isLeaf=True)
353 expNode['label']='which was just as good'
354 field = field.nextSibling
355 elif child.tagName == 'expectations':
356
357 subNode = self.findNode('Expected countermoves:',parent)
358 subNode.deleteChildren()
359 subChild = child.firstChild
360 while subChild:
361 if not subChild is child.firstChild:
362
363 assert subChild.tagName == 'turn'
364 name = str(subChild.getAttribute('agent'))
365 option = extractAction(subChild)
366 label = '%s %s' % (name,actionString(option))
367 subNode.addChild(widget,label=label,isLeaf=True)
368 subChild = subChild.nextSibling
369 elif child.tagName == 'decision':
370 pass
371 else:
372 print 'Unknown explanation field:',child.tagName
373 child = child.nextSibling
374
375 - def addGoals(self,label,node,keys,goals):
376 widget = self.component('Tree')
377 expNode = node.addChild(widget,label=label)
378 expNode['expanded'] = True
379 for key in keys:
380 if goals[key] > epsilon:
381 goalStr = 'maximize '
382 else:
383 goalStr = 'minimize '
384 goalStr += str(key)
385 expNode.addChild(widget,label=goalStr,isLeaf=True)
386
388 """Extracts an effect from an XML element and adds the corresponding subtree to the given parent tree
389 @param element: the XML effect description
390 @type element: Element
391 @param parent: the node to add the effect below
392 @type parent: L{Node}
393 """
394 widget = self.component('Tree')
395 node = self.findNode('Effect',parent)
396 node.deleteChildren()
397 child = element.firstChild
398 while child:
399 if child.tagName == 'state':
400
401 elements = child.getElementsByTagName('distribution')
402 assert len(elements) == 1
403 distribution = Distribution()
404 distribution.parse(elements[0],KeyedVector)
405 if len(distribution) > 1:
406 for row,prob in distribution.items():
407 subNode = node.addChild(widget,label='with probability %5.3f' % (prob))
408 for key,value in row.items():
409 if value > 0.0:
410 label = '%s increases by %5.3f' % (str(key),value)
411 subNode.addChild(widget,label=label,isLeaf=True)
412 elif value < 0.0:
413 subNode.addChild(widget,label='%s decreases by %5.3f' % (str(key),-value),isLeaf=True)
414 if len(subNode.child) == 0:
415 subNode.addChild(widget,label='no change',isLeaf=True)
416 else:
417 row = distribution.expectation()
418 for key,value in row.items():
419 if value > 0.0:
420 node.addChild(widget,label='%s increases by %5.3f' % (str(key),value),isLeaf=True)
421 elif value < 0.0:
422 node.addChild(widget,label='%s decreases by %5.3f' % (str(key),-value),isLeaf=True)
423 if len(node['children']) == 0:
424 node.addChild(widget,label='no change',isLeaf=True)
425 elif child.tagName == 'hearer':
426 name = str(child.getAttribute('agent'))
427 isLeaf=False
428 if str(child.getAttribute('decision')) == str(True):
429 label = '%s accept(s) message' % (name)
430 else:
431 label = '%s reject(s) message' % (name)
432 if str(child.getAttribute('forced')) == str(True):
433 isLeaf=True
434 label += ' (forced)'
435 subNode = node.addChild(widget,label=label,isLeaf=isLeaf)
436 subChild = child.firstChild
437 while subChild:
438 assert subChild.tagName == 'factor'
439 if str(subChild.getAttribute('positive')) == str(True):
440 label = 'sufficient '
441 else:
442 label = 'insufficient '
443 label += str(subChild.getAttribute('type'))
444 subNode.addChild(widget,label=label,isLeaf=True)
445 subChild = subChild.nextSibling
446 child = child.nextSibling
447
449 msg = 'Would you like to confirm this decision as the desired one? '\
450 'If you would like to specify another option as desired, '\
451 'you can now do so by clicking on the alternative '\
452 'action below.'
453 if tkMessageBox.askyesno('Fitting',msg):
454 self['fitCommand'](agent,option,step-1)
455
457 widget = self.component('Tree')
458
459 total = int(element.getAttribute('objectives'))
460 if total == 0:
461
462 return
463 count = total - int(element.getAttribute('violations'))
464 label = 'Satisfied %d / %d objectives' % (count,total)
465 root = self.findNode('Satisfied',parent)
466 root.deleteChildren()
467 root['label'] = label
468 child = element.firstChild
469
470 violations = []
471 while child:
472 if child.tagName == 'objective':
473 objective = {'who':str(child.getAttribute('who')),
474 'what':str(child.getAttribute('what')),
475 'how':str(child.getAttribute('how'))}
476 suggestions = []
477 suggestion = child.firstChild
478 while suggestion:
479 assert suggestion.tagName == 'suggestion'
480 values = {}
481 belief = suggestion.firstChild
482 while belief:
483 key = Key()
484 key = key.parse(belief)
485 values[key] = {'min':float(belief.getAttribute('min')),
486 'max':float(belief.getAttribute('max')),
487 'key':key,
488 'violator':agent}
489 belief = belief.nextSibling
490 suggestions.append(values)
491 suggestion = suggestion.nextSibling
492 violations.append((objective,suggestions))
493 elif child.tagName == 'distribution':
494 state = Distribution()
495 state.parse(child,KeyedVector)
496 state = state.expectation()
497 else:
498 print 'Unknown tag in suggestions:',child.tagName
499 child = child.nextSibling
500
501 for objective,suggestions in violations:
502 node = root.addChild(widget,label='To satisfy %s %s:' % (objective['how'],objective['what']))
503 first = True
504 messages = {}
505 for suggestion in suggestions:
506
507 beliefs = []
508 for key,threshold in suggestion.items():
509 if state[key] < threshold['min']:
510
511 if self['expert']:
512 sugg = '%s is greater than %5.3f' % \
513 (str(key),threshold['min'])
514 elif abs(threshold['min']) < epsilon:
515 sugg = '%s: positive %s' % \
516 (key['entity'],key['feature'])
517 else:
518 sugg = '%s: increased %s' % \
519 (key['entity'],key['feature'])
520 suggestion[key]['label'] = sugg
521 beliefs.append(sugg)
522 elif state[key] > threshold['max']:
523
524 if self['expert']:
525 sugg = '%s is less than %5.3f' % \
526 (str(key),threshold['max'])
527 elif abs(threshold['max']) < epsilon:
528 sugg = '%s: no %s' % \
529 (key['entity'],key['feature'])
530 else:
531 sugg = '%s: decreased %s' % \
532 (key['entity'],key['feature'])
533 suggestion[key]['label'] = sugg
534 beliefs.append(sugg)
535 if len(beliefs) > 0:
536 if first:
537 first = False
538 else:
539
540 node.addChild(widget,label=' ',isLeaf=True)
541 for key,args in suggestion.items():
542 if args.has_key('label'):
543 messages[args['label']] = args
544 button = node.addChild(widget,label=args['label'],
545 isLeaf=True)
546 if self['msgCommand']:
547 button['action'] = lambda l,e,s=self,o=args:\
548 s.sendMenu(e,o)
549
550 args = {'violator':agent,
551 'messages':messages.values()}
552 node['action'] = lambda l,e,s=self,o=args:s.sendMenu(e,o)
553
568
598