Skip to content

Commit 81da4fd

Browse files
Adapt task_score() to also generate contestant-appropriate info
A contestant can see two task_score: public and tokened. Public looks at the public score for all submissions; tokened looks at the total score for tokened submissions. Note that we cannot handle the tokened case restricting the list of submissions, because that could change the last submission, which is used in some scoremodes.
1 parent 1ef3f1c commit 81da4fd

2 files changed

Lines changed: 160 additions & 57 deletions

File tree

cms/grading/scoring.py

Lines changed: 69 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,19 @@ def compare(a, b):
109109

110110
# Computing global scores (for ranking).
111111

112-
def task_score(participation, task):
112+
def task_score(participation, task, public=False, only_tokened=False):
113113
"""Return the score of a contest's user on a task.
114114
115115
participation (Participation): the user and contest for which to
116116
compute the score.
117117
task (Task): the task for which to compute the score.
118+
public (bool): if True, compute the public score (that is, the one
119+
discoverable looking only at the results of public testcases) instead
120+
of the full score.
121+
only_tokened (bool): if True, compute the score discoverable only looking
122+
at the results of tokened submissions (that is, the score that the user
123+
would obtain if all non-tokened submissions scored 0.0, or equivalently
124+
had not been scored yet).
118125
119126
return ((float, bool)): the score of user on task, and True if not
120127
all submissions of the participation in the task have been scored.
@@ -128,6 +135,12 @@ def task_score(participation, task):
128135
# submission_results table. Doing so means that this function should incur
129136
# no exta database queries.
130137

138+
if public and only_tokened:
139+
raise ValueError(
140+
"Requested public task score restricted to tokened submissions. "
141+
"This is a programming error: users have access to all public "
142+
"scores regardless of token status.")
143+
131144
submissions = [s for s in participation.submissions
132145
if s.task is task and s.official]
133146
if len(submissions) == 0:
@@ -137,54 +150,67 @@ def task_score(participation, task):
137150
(s, s.get_result(task.active_dataset))
138151
for s in sorted(submissions, key=lambda s: s.timestamp)]
139152

153+
score_details_tokened = []
154+
partial = False
155+
for s, sr in submissions_and_results:
156+
if sr is None or not sr.scored():
157+
partial = True
158+
score, score_details = None, None
159+
elif public:
160+
score, score_details = sr.public_score, sr.public_score_details
161+
elif only_tokened and not s.tokened():
162+
# If the caller wants the only_tokened score and this submission is
163+
# not tokened, the score mode should ignore its score. To do so, we
164+
# send to the score mode what we would send if it wasn't already
165+
# scored.
166+
score, score_details = None, None
167+
else:
168+
score, score_details = sr.score, sr.score_details
169+
score_details_tokened.append((score, score_details, s.tokened()))
170+
140171
if task.score_mode == SCORE_MODE_MAX:
141-
return _task_score_max(submissions_and_results)
172+
return _task_score_max(score_details_tokened), partial
142173
if task.score_mode == SCORE_MODE_MAX_SUBTASK:
143-
return _task_score_max_subtask(submissions_and_results)
174+
return _task_score_max_subtask(score_details_tokened), partial
144175
elif task.score_mode == SCORE_MODE_MAX_TOKENED_LAST:
145-
return _task_score_max_tokened_last(submissions_and_results)
176+
return _task_score_max_tokened_last(score_details_tokened), partial
146177
else:
147178
raise ValueError("Unknown score mode '%s'" % task.score_mode)
148179

149180

150-
def _task_score_max_tokened_last(submissions_and_results):
181+
def _task_score_max_tokened_last(score_details_tokened):
151182
"""Compute score using the "max tokened last" score mode.
152183
153184
This was used in IOI 2010-2012. The score of a participant on a task is
154185
the maximum score amongst all tokened submissions and the last submission
155186
(not yet computed scores count as 0.0).
156187
157-
submissions_and_results ([(Submission, SubmissionResult|None)]): list of
158-
all submissions and their results for the participant on the task (on
159-
the dataset of interest); result is None if not available (that is,
160-
if the submission has not been compiled).
188+
score_details_tokened ([(float|None, object|None, bool)]): a tuple for each
189+
submission of the user in the task, containing score, score details
190+
(each None if not scored yet) and if the submission was tokened.
161191
162-
return ((float, bool)): (score, partial), same as task_score().
192+
return (float): the score.
163193
164194
"""
165-
partial = False
166195

167196
# The score of the last submission (if computed, otherwise 0.0). Note that
168197
# partial will be set to True in the next loop.
169-
last_score = 0.0
170-
_, last_sr = submissions_and_results[-1]
171-
if last_sr is not None and last_sr.scored():
172-
last_score = last_sr.score
198+
last_score, _, _ = score_details_tokened[-1]
199+
if last_score is None:
200+
last_score = 0.0
173201

174202
# The maximum score amongst the tokened submissions (not yet computed
175203
# scores count as 0.0).
176204
max_tokened_score = 0.0
177-
for s, sr in submissions_and_results:
178-
if sr is not None and sr.scored():
179-
if s.tokened():
180-
max_tokened_score = max(max_tokened_score, sr.score)
181-
else:
182-
partial = True
205+
for score, _, tokened in score_details_tokened:
206+
if score is not None:
207+
if tokened:
208+
max_tokened_score = max(max_tokened_score, score)
183209

184-
return max(last_score, max_tokened_score), partial
210+
return max(last_score, max_tokened_score)
185211

186212

187-
def _task_score_max_subtask(submissions_and_results):
213+
def _task_score_max_subtask(score_details_tokened):
188214
"""Compute score using the "max subtask" score mode.
189215
190216
This has been used in IOI since 2017. The score of a participant on a
@@ -196,68 +222,60 @@ def _task_score_max_subtask(submissions_and_results):
196222
this is not true, the score mode will work as if the task had a single
197223
subtask.
198224
199-
submissions_and_results ([(Submission, SubmissionResult|None)]): list of
200-
all submissions and their results for the participant on the task (on
201-
the dataset of interest); result is None if not available (that is,
202-
if the submission has not been compiled).
225+
score_details_tokened ([(float|None, object|None, bool)]): a tuple for each
226+
submission of the user in the task, containing score, score details
227+
(each None if not scored yet) and if the submission was tokened.
203228
204-
return ((float, bool)): (score, partial), same as task_score().
229+
return (float): the score.
205230
206231
"""
207232
# Maximum score for each subtask (not yet computed scores count as 0.0).
208233
max_scores = {}
209234

210-
partial = False
211-
for _, sr in submissions_and_results:
212-
if sr is None or not sr.scored():
213-
partial = True
235+
for score, details, _ in score_details_tokened:
236+
if score is None:
214237
continue
215238

216-
if sr.score_details == [] and sr.score == 0.0:
239+
if details == [] and score == 0.0:
217240
# Submission did not compile, ignore it.
218241
continue
219242

220243
try:
221244
subtask_scores = dict(
222245
(subtask["idx"],
223246
subtask["score_fraction"] * subtask["max_score"])
224-
for subtask in sr.score_details
225-
)
247+
for subtask in details)
226248
except Exception:
227249
subtask_scores = None
228250

229251
if subtask_scores is None or len(subtask_scores) == 0:
230252
# Task's score type is not group, assume a single subtask.
231-
subtask_scores = {1: sr.score}
253+
subtask_scores = {1: score}
232254

233255
for idx, score in iteritems(subtask_scores):
234256
max_scores[idx] = max(max_scores.get(idx, 0.0), score)
235257

236-
return sum(itervalues(max_scores)), partial
258+
return sum(itervalues(max_scores))
237259

238260

239-
def _task_score_max(submissions_and_results):
261+
def _task_score_max(score_details_tokened):
240262
"""Compute score using the "max" score mode.
241263
242264
This was used in IOI 2013-2016. The score of a participant on a task is
243265
the maximum score amongst all submissions (not yet computed scores count
244266
as 0.0).
245267
246-
submissions_and_results ([(Submission, SubmissionResult|None)]): list of
247-
all submissions and their results for the participant on the task (on
248-
the dataset of interest); result is None if not available (that is,
249-
if the submission has not been compiled).
268+
score_details_tokened ([(float|None, object|None, bool)]): a tuple for each
269+
submission of the user in the task, containing score, score details
270+
(each None if not scored yet) and if the submission was tokened.
250271
251-
return ((float, bool)): (score, partial), same as task_score().
272+
return (float): the score.
252273
253274
"""
254-
partial = False
255-
score = 0.0
275+
max_score = 0.0
256276

257-
for _, sr in submissions_and_results:
258-
if sr is not None and sr.scored():
259-
score = max(score, sr.score)
260-
else:
261-
partial = True
277+
for score, _, _ in score_details_tokened:
278+
if score is not None:
279+
max_score = max(max_score, score)
262280

263-
return score, partial
281+
return max_score

cmstestsuite/unit_tests/grading/scoring_test.py

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,15 @@ def setUp(self):
5454
def at(self, timestamp):
5555
return self.timestamp + timedelta(seconds=timestamp)
5656

57-
def call(self):
58-
return task_score(self.participation, self.task)
59-
60-
def add_result(self, timestamp, score, tokened=False, score_details=None):
57+
def call(self, public=False, only_tokened=False):
58+
return task_score(self.participation, self.task,
59+
public=public, only_tokened=only_tokened)
60+
61+
def add_result(self, timestamp, score, tokened=False, score_details=None,
62+
public_score=None, public_score_details=None):
63+
public_score = public_score if public_score is not None else 0.0
64+
public_score_details = public_score_details \
65+
if public_score_details is not None else []
6166
score_details = score_details if score_details is not None else []
6267
submission = self.add_submission(
6368
participation=self.participation,
@@ -67,9 +72,9 @@ def add_result(self, timestamp, score, tokened=False, score_details=None):
6772
# must be set to declare the submission result as scored.
6873
self.add_submission_result(submission, self.task.active_dataset,
6974
score=score,
70-
public_score=score,
75+
public_score=public_score,
7176
score_details=score_details,
72-
public_score_details=score_details,
77+
public_score_details=public_score_details,
7378
ranking_score_details=[])
7479
if tokened:
7580
self.add_token(timestamp=timestamp, submission=submission)
@@ -149,6 +154,20 @@ def test_all_unscored(self):
149154
self.session.flush()
150155
self.assertEqual(self.call(), (0.0, True))
151156

157+
def test_public(self):
158+
self.add_result(self.at(1), 44.4, tokened=True, public_score=4.4)
159+
self.add_result(self.at(2), 66.6, tokened=False, public_score=66.6)
160+
self.add_result(self.at(3), 11.1, tokened=False, public_score=11.1)
161+
self.session.flush()
162+
self.assertEqual(self.call(public=True), (11.1, False))
163+
164+
def test_only_tokened(self):
165+
self.add_result(self.at(1), 11.1, tokened=True)
166+
self.add_result(self.at(2), 66.6, tokened=False)
167+
self.add_result(self.at(3), 44.4, tokened=False)
168+
self.session.flush()
169+
self.assertEqual(self.call(only_tokened=True), (11.1, False))
170+
152171

153172
class TestTaskScoreMaxSubtask(TaskScoreMixin, unittest.TestCase):
154173
"""Tests for task_score() using the max_subtask score mode."""
@@ -247,6 +266,60 @@ def test_rounding(self):
247266
self.session.flush()
248267
self.assertEqual(self.call(), (80 + 0.0004, False))
249268

269+
def test_public(self):
270+
self.add_result(self.at(1),
271+
30 * 1.0 + 40 * 1.0 + 30 * 1.0,
272+
score_details=[
273+
self.subtask(3, 30, 1.0),
274+
self.subtask(2, 40, 1.0),
275+
self.subtask(1, 30, 1.0),
276+
],
277+
public_score=30 * 0.2 + 40 * 0.5 + 30 * 0.1,
278+
public_score_details=[
279+
self.subtask(3, 30, 0.2),
280+
self.subtask(2, 40, 0.5),
281+
self.subtask(1, 30, 0.1),
282+
])
283+
self.add_result(self.at(2),
284+
30 * 1.0 + 40 * 1.0 + 30 * 1.0,
285+
score_details=[
286+
self.subtask(2, 40, 1.0),
287+
self.subtask(1, 30, 1.0),
288+
self.subtask(3, 30, 1.0),
289+
],
290+
public_score=30 * 0.1 + 40 * 0.5 + 30 * 0.2,
291+
public_score_details=[
292+
self.subtask(2, 40, 0.5),
293+
self.subtask(1, 30, 0.2),
294+
self.subtask(3, 30, 0.1),
295+
])
296+
self.session.flush()
297+
self.assertEqual(self.call(public=True),
298+
(30 * 0.2 + 40 * 0.5 + 30 * 0.2, False))
299+
300+
def test_only_tokened(self):
301+
self.add_result(self.at(1), 30 * 0.2 + 40 * 0.5 + 30 * 0.1,
302+
score_details=[
303+
self.subtask(3, 30, 0.2),
304+
self.subtask(2, 40, 0.5),
305+
self.subtask(1, 30, 0.1),
306+
], tokened=True)
307+
self.add_result(self.at(2), 30 * 0.1 + 40 * 0.5 + 30 * 0.2,
308+
score_details=[
309+
self.subtask(2, 40, 0.5),
310+
self.subtask(1, 30, 0.2),
311+
self.subtask(3, 30, 0.1),
312+
], tokened=True)
313+
self.add_result(self.at(3), 30 * 1.0 + 40 * 1.0 + 30 * 1.0,
314+
score_details=[
315+
self.subtask(2, 40, 1.0),
316+
self.subtask(1, 30, 1.0),
317+
self.subtask(3, 30, 1.0),
318+
], tokened=False)
319+
self.session.flush()
320+
self.assertEqual(self.call(only_tokened=True),
321+
(30 * 0.2 + 40 * 0.5 + 30 * 0.2, False))
322+
250323

251324
class TestTaskScoreMax(TaskScoreMixin, unittest.TestCase):
252325
"""Tests for task_score() using the max score mode."""
@@ -290,6 +363,18 @@ def test_all_unscored(self):
290363
self.session.flush()
291364
self.assertEqual(self.call(), (0.0, True))
292365

366+
def test_public(self):
367+
self.add_result(self.at(1), 44.4, tokened=False, public_score=44.4)
368+
self.add_result(self.at(2), 66.6, tokened=False, public_score=6.6)
369+
self.session.flush()
370+
self.assertEqual(self.call(public=True), (44.4, False))
371+
372+
def test_only_tokened(self):
373+
self.add_result(self.at(1), 44.4, tokened=True)
374+
self.add_result(self.at(2), 66.6, tokened=False)
375+
self.session.flush()
376+
self.assertEqual(self.call(only_tokened=True), (44.4, False))
377+
293378

294379
if __name__ == "__main__":
295380
unittest.main()

0 commit comments

Comments
 (0)