comparison problem.py @ 76:0e5ae28e0b2b

Points are now weighted on a test context basis In particular, this has allowed for simple extensions to the format of testconf to award points to whole test groups without at the same time compromising the future ability of giving partial score for correct but slow solutions. Specifically, the groupweight configuration variable has been added and normally has the format {groupindex: points} where groupindex is the group's index in the tests configuration variable. The backwards incompatible change is that test contexts are no longer guaranteed to learn the score awarded or the maximum possible score for every test case and may instead be notified about them in batches. In other news, the pointmap and groupweight configuration variables can (now) be given as sequences in addition to mappings. (Technically, the distinction currently made is dict versus everything else.) Items of a sequence pointmap/groupweight correspond directly to the test cases/ groups defined in the tests configuration variable; in particular, when groups are used, tests=[1],[2,3];pointmap={1:1,2:2,3:3} can now be written as pointmap=tests=[1],[2,3]. Missing items are handled in the same way in which they are handled when the variable is a mapping. Note that the items of groupweight correspond to whole test groups rather than individual test cases. In other news again, the wording of problem total lines has been changed from '<unweighted> points; weighted score: <weighted>' to '<weighted> points (<unweighted> before weighting)', and group total lines now properly report fractional numbers of points (this is a bug fix).
author Oleg Oshmyan <chortos@inbox.lv>
date Sat, 08 Jan 2011 16:03:35 +0200
parents 007f7eb6fb2b
children 69eadc60f4e2
comparison
equal deleted inserted replaced
75:007f7eb6fb2b 76:0e5ae28e0b2b
50 pass 50 pass
51 51
52 test_context_end = object() 52 test_context_end = object()
53 53
54 class TestGroup(TestContext): 54 class TestGroup(TestContext):
55 __slots__ = 'case', 'log', 'correct', 'allcorrect', 'real', 'max', 'ntotal', 'nvalued', 'ncorrect', 'ncorrectvalued' 55 __slots__ = 'points', 'case', 'log', 'correct', 'allcorrect', 'real', 'max', 'ntotal', 'nvalued', 'ncorrect', 'ncorrectvalued'
56 56
57 def __init__(self): 57 def __init__(self, points=None):
58 self.points = points
58 self.real = self.max = self.ntotal = self.nvalued = self.ncorrect = self.ncorrectvalued = 0 59 self.real = self.max = self.ntotal = self.nvalued = self.ncorrect = self.ncorrectvalued = 0
59 self.allcorrect = True 60 self.allcorrect = True
60 self.log = [] 61 self.log = []
61 62
62 def case_start(self, case): 63 def case_start(self, case):
63 self.case = case 64 self.case = case
64 self.correct = False 65 self.correct = False
65 self.ntotal += 1 66 self.ntotal += 1
66 self.max += case.points
67 if case.points: 67 if case.points:
68 self.nvalued += 1 68 self.nvalued += 1
69 69
70 def case_correct(self): 70 def case_correct(self):
71 self.correct = True 71 self.correct = True
72 self.ncorrect += 1 72 self.ncorrect += 1
73 if self.case.points: 73 if self.case.points:
74 self.ncorrectvalued += 1 74 self.ncorrectvalued += 1
75 75
76 def case_end(self, granted): 76 def case_end(self):
77 self.log.append((self.case, self.correct, granted)) 77 self.log.append((self.case, self.correct))
78 self.real += granted
79 del self.case 78 del self.case
80 if not self.correct: 79 if not self.correct:
81 self.allcorrect = False 80 self.allcorrect = False
82 81
82 def score(self, real, max):
83 self.real += real
84 self.max += max
85
83 def end(self): 86 def end(self):
84 say('Group total: %d/%d tests; %d/%d points' % (self.ncorrect, self.ntotal, self.real if self.allcorrect else 0, self.max)) 87 if not self.allcorrect:
88 self.real = 0
89 if self.points is not None and self.points != self.max:
90 max, weighted = self.points, self.real * self.points / self.max if self.max else 0
91 before_weighting = ' (%g/%g before weighting)' % (self.real, self.max)
92 else:
93 max, weighted = self.max, self.real
94 before_weighting = ''
95 say('Group total: %d/%d tests, %g/%g points%s' % (self.ncorrect, self.ntotal, weighted, max, before_weighting))
85 # No real need to flush stdout, as it will anyway be flushed in a moment, 96 # No real need to flush stdout, as it will anyway be flushed in a moment,
86 # when either the problem total or the next test case's ID is printed 97 # when either the problem total or the next test case's ID is printed
87 if self.allcorrect: 98 return weighted, max, self.log
88 return self.log
89 else:
90 return ((case, correct, 0) for case, correct, granted in self.log)
91 99
92 class Problem(object): 100 class Problem(object):
93 __slots__ = 'name', 'config', 'cache', 'testcases' 101 __slots__ = 'name', 'config', 'cache', 'testcases'
94 102
95 def __init__(prob, name): 103 def __init__(prob, name):
109 case = None 117 case = None
110 try: 118 try:
111 contexts = deque((TestGroup(),)) 119 contexts = deque((TestGroup(),))
112 for case in prob.testcases: 120 for case in prob.testcases:
113 if case is test_context_end: 121 if case is test_context_end:
114 for case, correct, granted in contexts.pop().end(): 122 real, max, log = contexts.pop().end()
123 for case, correct in log:
115 contexts[-1].case_start(case) 124 contexts[-1].case_start(case)
116 if correct: 125 if correct:
117 contexts[-1].case_correct() 126 contexts[-1].case_correct()
118 contexts[-1].case_end(granted) 127 contexts[-1].case_end()
128 contexts[-1].score(real, max)
119 continue 129 continue
120 elif isinstance(case, TestContext): 130 elif isinstance(case, TestContext):
121 contexts.append(case) 131 contexts.append(case)
122 continue 132 continue
123 contexts[-1].case_start(case) 133 contexts[-1].case_start(case)
179 verdict = 'wrong answer' + comment 189 verdict = 'wrong answer' + comment
180 else: 190 else:
181 verdict = 'partly correct' + comment 191 verdict = 'partly correct' + comment
182 granted *= case.points 192 granted *= case.points
183 say('%g/%g, %s' % (granted, case.points, verdict)) 193 say('%g/%g, %s' % (granted, case.points, verdict))
184 contexts[-1].case_end(granted) 194 contexts[-1].case_end()
195 contexts[-1].score(granted, case.points)
185 weighted = contexts[0].real * prob.config.taskweight / contexts[0].max if contexts[0].max else 0 196 weighted = contexts[0].real * prob.config.taskweight / contexts[0].max if contexts[0].max else 0
197 before_weighting = valued = ''
198 if prob.config.taskweight != contexts[0].max:
199 before_weighting = ' (%g/%g before weighting)' % (contexts[0].real, contexts[0].max)
186 if contexts[0].nvalued != contexts[0].ntotal: 200 if contexts[0].nvalued != contexts[0].ntotal:
187 say('Problem total: %d/%d tests (%d/%d valued); %g/%g points; weighted score: %g/%g' % (contexts[0].ncorrect, contexts[0].ntotal, contexts[0].ncorrectvalued, contexts[0].nvalued, contexts[0].real, contexts[0].max, weighted, prob.config.taskweight)) 201 valued = ' (%d/%d valued)' % (contexts[0].ncorrectvalued, contexts[0].nvalued)
188 else: 202 say('Problem total: %d/%d tests%s, %g/%g points%s' % (contexts[0].ncorrect, contexts[0].ntotal, valued, weighted, prob.config.taskweight, before_weighting))
189 say('Problem total: %d/%d tests; %g/%g points; weighted score: %g/%g' % (contexts[0].ncorrect, contexts[0].ntotal, contexts[0].real, contexts[0].max, weighted, prob.config.taskweight))
190 sys.stdout.flush() 203 sys.stdout.flush()
191 return weighted, prob.config.taskweight 204 return weighted, prob.config.taskweight
192 finally: 205 finally:
193 if options.erase and (not prob.config.stdio or case and case.validator): 206 if options.erase and (not prob.config.stdio or case and case.validator):
194 for var in 'in', 'out': 207 for var in 'in', 'out':