Mercurial > ~astiob > upreckon > hgweb
changeset 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 |
files | config.py problem.py testcases.py |
diffstat | 3 files changed, 79 insertions(+), 36 deletions(-) [+] |
line wrap: on
line diff
--- a/config.py Thu Jan 06 23:53:31 2011 +0200 +++ b/config.py Sat Jan 08 16:03:35 2011 +0200 @@ -33,6 +33,7 @@ 'padtests': 0, 'paddummies': 0, 'taskweight': 100, + 'groupweight': {}, 'pointmap': {}, 'stdio': False, 'dummyinname': '', @@ -139,17 +140,20 @@ module.path = os.path.join(os.path.curdir, problem_name) else: module.path = problem_name - newpointmap = {} - for key in module.pointmap: - if not options.legacy and isinstance(key, basestring): - newpointmap[key] = module.pointmap[key] - else: - try: - for k in key: - newpointmap[k] = module.pointmap[key] - except TypeError: - newpointmap[key] = module.pointmap[key] - module.pointmap = newpointmap + for name in 'pointmap', 'groupweight': + oldmap = getattr(module, name) + if isinstance(oldmap, dict): + newmap = {} + for key in oldmap: + if not options.legacy and isinstance(key, basestring): + newmap[key] = oldmap[key] + else: + try: + for k in key: + newmap[k] = oldmap[key] + except TypeError: + newmap[key] = oldmap[key] + setattr(module, name, newmap) if options.no_maxtime: module.maxtime = 0 sys.dont_write_bytecode = dwb
--- a/problem.py Thu Jan 06 23:53:31 2011 +0200 +++ b/problem.py Sat Jan 08 16:03:35 2011 +0200 @@ -52,9 +52,10 @@ test_context_end = object() class TestGroup(TestContext): - __slots__ = 'case', 'log', 'correct', 'allcorrect', 'real', 'max', 'ntotal', 'nvalued', 'ncorrect', 'ncorrectvalued' + __slots__ = 'points', 'case', 'log', 'correct', 'allcorrect', 'real', 'max', 'ntotal', 'nvalued', 'ncorrect', 'ncorrectvalued' - def __init__(self): + def __init__(self, points=None): + self.points = points self.real = self.max = self.ntotal = self.nvalued = self.ncorrect = self.ncorrectvalued = 0 self.allcorrect = True self.log = [] @@ -63,7 +64,6 @@ self.case = case self.correct = False self.ntotal += 1 - self.max += case.points if case.points: self.nvalued += 1 @@ -73,21 +73,29 @@ if self.case.points: self.ncorrectvalued += 1 - def case_end(self, granted): - self.log.append((self.case, self.correct, granted)) - self.real += granted + def case_end(self): + self.log.append((self.case, self.correct)) del self.case if not self.correct: self.allcorrect = False + def score(self, real, max): + self.real += real + self.max += max + def end(self): - say('Group total: %d/%d tests; %d/%d points' % (self.ncorrect, self.ntotal, self.real if self.allcorrect else 0, self.max)) + if not self.allcorrect: + self.real = 0 + if self.points is not None and self.points != self.max: + max, weighted = self.points, self.real * self.points / self.max if self.max else 0 + before_weighting = ' (%g/%g before weighting)' % (self.real, self.max) + else: + max, weighted = self.max, self.real + before_weighting = '' + say('Group total: %d/%d tests, %g/%g points%s' % (self.ncorrect, self.ntotal, weighted, max, before_weighting)) # No real need to flush stdout, as it will anyway be flushed in a moment, # when either the problem total or the next test case's ID is printed - if self.allcorrect: - return self.log - else: - return ((case, correct, 0) for case, correct, granted in self.log) + return weighted, max, self.log class Problem(object): __slots__ = 'name', 'config', 'cache', 'testcases' @@ -111,11 +119,13 @@ contexts = deque((TestGroup(),)) for case in prob.testcases: if case is test_context_end: - for case, correct, granted in contexts.pop().end(): + real, max, log = contexts.pop().end() + for case, correct in log: contexts[-1].case_start(case) if correct: contexts[-1].case_correct() - contexts[-1].case_end(granted) + contexts[-1].case_end() + contexts[-1].score(real, max) continue elif isinstance(case, TestContext): contexts.append(case) @@ -181,12 +191,15 @@ verdict = 'partly correct' + comment granted *= case.points say('%g/%g, %s' % (granted, case.points, verdict)) - contexts[-1].case_end(granted) + contexts[-1].case_end() + contexts[-1].score(granted, case.points) weighted = contexts[0].real * prob.config.taskweight / contexts[0].max if contexts[0].max else 0 + before_weighting = valued = '' + if prob.config.taskweight != contexts[0].max: + before_weighting = ' (%g/%g before weighting)' % (contexts[0].real, contexts[0].max) if contexts[0].nvalued != contexts[0].ntotal: - 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)) - else: - 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)) + valued = ' (%d/%d valued)' % (contexts[0].ncorrectvalued, contexts[0].nvalued) + say('Problem total: %d/%d tests%s, %g/%g points%s' % (contexts[0].ncorrect, contexts[0].ntotal, valued, weighted, prob.config.taskweight, before_weighting)) sys.stdout.flush() return weighted, prob.config.taskweight finally:
--- a/testcases.py Thu Jan 06 23:53:31 2011 +0200 +++ b/testcases.py Sat Jan 08 16:03:35 2011 +0200 @@ -680,8 +680,7 @@ __slots__ = () def end(self): say('Sample total: %d/%d tests' % (self.ncorrect, self.ntotal)) - return self.log - + return 0, 0, self.log def load_problem(prob, _types={'batch' : BatchTestCase, 'outonly' : OutputOnlyTestCase, @@ -717,12 +716,37 @@ prob.config.tests = newtests del newtests + # Even if they have duplicate test identifiers, we must honour sequence pointmaps + if isinstance(prob.config.pointmap, dict): + def getpoints(i, j, k=None): + try: + return prob.config.pointmap[i] + except KeyError: + try: + return prob.config.pointmap[None] + except KeyError: + return prob.config.maxexitcode or 1 + elif prob.config.usegroups: + def getpoints(i, j, k): + try: + return prob.config.pointmap[k][j] + except LookupError: + return prob.config.maxexitcode or 1 + else: + def getpoints(i, j): + try: + return prob.config.pointmap[j] + except LookupError: + return prob.config.maxexitcode or 1 + # First get prob.cache.padoutput right, # then yield the actual test cases for i in prob.config.dummies: s = 'sample ' + str(i).zfill(prob.config.paddummies) prob.cache.padoutput = max(prob.cache.padoutput, len(s)) if prob.config.usegroups: + if not isinstance(prob.config.groupweight, dict): + prob.config.groupweight = dict(enumerate(prob.config.groupweight)) for group in prob.config.tests: for i in group: s = str(i).zfill(prob.config.padtests) @@ -732,11 +756,13 @@ s = str(i).zfill(prob.config.paddummies) yield _types[prob.config.kind](prob, s, True, 0) yield problem.test_context_end - for group in prob.config.tests: - yield problem.TestGroup() - for i in group: + for k, group in enumerate(prob.config.tests): + if not group: + continue + yield problem.TestGroup(prob.config.groupweight.get(k, prob.config.groupweight.get(None))) + for j, i in enumerate(group): s = str(i).zfill(prob.config.padtests) - yield _types[prob.config.kind](prob, s, False, prob.config.pointmap.get(i, prob.config.pointmap.get(None, prob.config.maxexitcode if prob.config.maxexitcode else 1))) + yield _types[prob.config.kind](prob, s, False, getpoints(i, j, k)) yield problem.test_context_end else: for i in prob.config.tests: @@ -745,6 +771,6 @@ for i in prob.config.dummies: s = str(i).zfill(prob.config.paddummies) yield _types[prob.config.kind](prob, s, True, 0) - for i in prob.config.tests: + for j, i in enumerate(prob.config.tests): s = str(i).zfill(prob.config.padtests) - yield _types[prob.config.kind](prob, s, False, prob.config.pointmap.get(i, prob.config.pointmap.get(None, prob.config.maxexitcode if prob.config.maxexitcode else 1))) \ No newline at end of file + yield _types[prob.config.kind](prob, s, False, getpoints(i, j)) \ No newline at end of file