Skip to content

Commit e324d25

Browse files
Add subtask max score mode in core CMS
1 parent 8bdef59 commit e324d25

8 files changed

Lines changed: 183 additions & 16 deletions

File tree

cms/db/task.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
# Contest Management System - http://cms-dev.github.io/
55
# Copyright © 2010-2014 Giovanni Mascellani <mascellani@poisson.phc.unipi.it>
6-
# Copyright © 2010-2014 Stefano Maggiolo <s.maggiolo@gmail.com>
6+
# Copyright © 2010-2018 Stefano Maggiolo <s.maggiolo@gmail.com>
77
# Copyright © 2010-2012 Matteo Boscariol <boscarim@hotmail.com>
88
# Copyright © 2012-2018 Luca Wehrstedt <luca.wehrstedt@gmail.com>
99
# Copyright © 2013 Bernard Blackham <bernard@largestprime.net>
@@ -46,7 +46,8 @@
4646

4747
from cms import TOKEN_MODE_DISABLED, TOKEN_MODE_FINITE, TOKEN_MODE_INFINITE, \
4848
FEEDBACK_LEVEL_FULL, FEEDBACK_LEVEL_RESTRICTED
49-
from cmscommon.constants import SCORE_MODE_MAX, SCORE_MODE_MAX_TOKENED_LAST
49+
from cmscommon.constants import \
50+
SCORE_MODE_MAX, SCORE_MODE_MAX_SUBTASK, SCORE_MODE_MAX_TOKENED_LAST
5051

5152
from . import Codename, Filename, FilenameSchemaArray, Digest, Base, Contest
5253

@@ -216,7 +217,9 @@ class Task(Base):
216217

217218
# Score mode for the task.
218219
score_mode = Column(
219-
Enum(SCORE_MODE_MAX_TOKENED_LAST, SCORE_MODE_MAX,
220+
Enum(SCORE_MODE_MAX_TOKENED_LAST,
221+
SCORE_MODE_MAX,
222+
SCORE_MODE_MAX_SUBTASK,
220223
name="score_mode"),
221224
nullable=False,
222225
default=SCORE_MODE_MAX_TOKENED_LAST)

cms/grading/scoring.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@
3030
from __future__ import unicode_literals
3131
from future.builtins.disabled import * # noqa
3232
from future.builtins import * # noqa
33+
from six import iteritems, itervalues
3334

3435
from collections import namedtuple
3536

3637
from sqlalchemy.orm import joinedload
3738

3839
from cms.db import Submission
39-
from cmscommon.constants import SCORE_MODE_MAX, SCORE_MODE_MAX_TOKENED_LAST
40+
from cmscommon.constants import \
41+
SCORE_MODE_MAX, SCORE_MODE_MAX_SUBTASK, SCORE_MODE_MAX_TOKENED_LAST
4042

4143

4244
__all__ = [
@@ -137,6 +139,8 @@ def task_score(participation, task):
137139

138140
if task.score_mode == SCORE_MODE_MAX:
139141
return _task_score_max(submissions_and_results)
142+
if task.score_mode == SCORE_MODE_MAX_SUBTASK:
143+
return _task_score_max_subtask(submissions_and_results)
140144
elif task.score_mode == SCORE_MODE_MAX_TOKENED_LAST:
141145
return _task_score_max_tokened_last(submissions_and_results)
142146
else:
@@ -180,6 +184,58 @@ def _task_score_max_tokened_last(submissions_and_results):
180184
return max(last_score, max_tokened_score), partial
181185

182186

187+
def _task_score_max_subtask(submissions_and_results):
188+
"""Compute score using the "max subtask" score mode.
189+
190+
This has been used in IOI since 2017. The score of a participant on a
191+
task is the sum, over the subtasks, of the maximum score amongst all
192+
submissions for that subtask (not yet computed scores count as 0.0).
193+
194+
If this score mode is selected, all tasks should be children of
195+
ScoreTypeGroup, or follow the same format for their score details. If
196+
this is not true, the score mode will work as if the task had a single
197+
subtask.
198+
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).
203+
204+
return ((float, bool)): (score, partial), same as task_score().
205+
206+
"""
207+
# Maximum score for each subtask (not yet computed scores count as 0.0).
208+
max_scores = {}
209+
210+
partial = False
211+
for _, sr in submissions_and_results:
212+
if sr is None or not sr.scored():
213+
partial = True
214+
continue
215+
216+
if sr.score_details == [] and sr.score == 0.0:
217+
# Submission did not compile, ignore it.
218+
continue
219+
220+
try:
221+
subtask_scores = dict(
222+
(subtask["idx"],
223+
subtask["score_fraction"] * subtask["max_score"])
224+
for subtask in sr.score_details
225+
)
226+
except Exception:
227+
subtask_scores = None
228+
229+
if subtask_scores is None or len(subtask_scores) == 0:
230+
# Task's score type is not group, assume a single subtask.
231+
subtask_scores = {1: sr.score}
232+
233+
for idx, score in iteritems(subtask_scores):
234+
max_scores[idx] = max(max_scores.get(idx, 0.0), score)
235+
236+
return sum(itervalues(max_scores)), partial
237+
238+
183239
def _task_score_max(submissions_and_results):
184240
"""Compute score using the "max" score mode.
185241

cms/server/admin/templates/task.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,8 @@ <h2 id="title_task_configuration" class="toggling_on">Task configuration</h2>
257257
<td>
258258
<select name="score_mode">
259259
<option value="{{ SCORE_MODE_MAX_TOKENED_LAST }}" {{ "selected" if task.score_mode == SCORE_MODE_MAX_TOKENED_LAST else "" }}>Use best among tokened and last submissions (IOI 2010-2012)</option>
260-
<option value="{{ SCORE_MODE_MAX }}" {{ "selected" if task.score_mode == SCORE_MODE_MAX else "" }}>Use best among all submissions (IOI 2013-)</option>
260+
<option value="{{ SCORE_MODE_MAX }}" {{ "selected" if task.score_mode == SCORE_MODE_MAX else "" }}>Use best among all submissions (IOI 2013-2016)</option>
261+
<option value="{{ SCORE_MODE_MAX_SUBTASK }}" {{ "selected" if task.score_mode == SCORE_MODE_MAX_SUBTASK else "" }}>Use the sum over each subtask of the best result for that subtask across all submissions (IOI 2017-)</option>
261262
</select>
262263
</td>
263264
</tr>

cms/server/jinja2_toolbox.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
from cms.grading import format_status_text
4646
from cms.grading.languagemanager import get_language
4747
from cms.locale import DEFAULT_TRANSLATION
48-
from cmscommon.constants import SCORE_MODE_MAX_TOKENED_LAST, SCORE_MODE_MAX
48+
from cmscommon.constants import \
49+
SCORE_MODE_MAX, SCORE_MODE_MAX_SUBTASK, SCORE_MODE_MAX_TOKENED_LAST
4950

5051

5152
@contextfilter
@@ -155,8 +156,9 @@ def instrument_generic_toolbox(env):
155156
env.globals["SubmissionResult"] = SubmissionResult
156157
env.globals["UserTestResult"] = UserTestResult
157158

158-
env.globals["SCORE_MODE_MAX"] = SCORE_MODE_MAX
159159
env.globals["SCORE_MODE_MAX_TOKENED_LAST"] = SCORE_MODE_MAX_TOKENED_LAST
160+
env.globals["SCORE_MODE_MAX"] = SCORE_MODE_MAX
161+
env.globals["SCORE_MODE_MAX_SUBTASK"] = SCORE_MODE_MAX_SUBTASK
160162

161163
env.globals["TOKEN_MODE_DISABLED"] = TOKEN_MODE_DISABLED
162164
env.globals["TOKEN_MODE_FINITE"] = TOKEN_MODE_FINITE

cmscommon/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,7 @@
2929

3030
# Maximum score amongst all submissions.
3131
SCORE_MODE_MAX = "max"
32+
# Sum of maximum score for each subtask over all submissions.
33+
SCORE_MODE_MAX_SUBTASK = "max_subtask"
3234
# Maximum score among all tokened submissions and the last submission.
3335
SCORE_MODE_MAX_TOKENED_LAST = "max_tokened_last"

cmscontrib/loaders/italy_yaml.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
# Contest Management System - http://cms-dev.github.io/
55
# Copyright © 2010-2014 Giovanni Mascellani <mascellani@poisson.phc.unipi.it>
6-
# Copyright © 2010-2017 Stefano Maggiolo <s.maggiolo@gmail.com>
6+
# Copyright © 2010-2018 Stefano Maggiolo <s.maggiolo@gmail.com>
77
# Copyright © 2010-2012 Matteo Boscariol <boscarim@hotmail.com>
88
# Copyright © 2013-2018 Luca Wehrstedt <luca.wehrstedt@gmail.com>
99
# Copyright © 2014-2018 William Di Luigi <williamdiluigi@gmail.com>
@@ -43,7 +43,8 @@
4343
from cms.db import Contest, User, Task, Statement, Attachment, Team, Dataset, \
4444
Manager, Testcase
4545
from cms.grading.languagemanager import LANGUAGES, HEADER_EXTS
46-
from cmscommon.constants import SCORE_MODE_MAX, SCORE_MODE_MAX_TOKENED_LAST
46+
from cmscommon.constants import \
47+
SCORE_MODE_MAX, SCORE_MODE_MAX_SUBTASK, SCORE_MODE_MAX_TOKENED_LAST
4748
from cmscommon.crypto import build_password
4849
from cmscommon.datetime import make_datetime
4950
from cmscontrib import touch
@@ -388,6 +389,8 @@ def get_task(self, get_statement=True):
388389

389390
if conf.get("score_mode", None) == SCORE_MODE_MAX:
390391
args["score_mode"] = SCORE_MODE_MAX
392+
elif conf.get("score_mode", None) == SCORE_MODE_MAX_SUBTASK:
393+
args["score_mode"] = SCORE_MODE_MAX_SUBTASK
391394
elif conf.get("score_mode", None) == SCORE_MODE_MAX_TOKENED_LAST:
392395
args["score_mode"] = SCORE_MODE_MAX_TOKENED_LAST
393396

cmstestsuite/unit_tests/grading/scoring_test.py

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
from cmstestsuite.unit_tests.databasemixin import DatabaseMixin
3636

3737
from cms.grading.scoring import task_score
38-
from cmscommon.constants import SCORE_MODE_MAX, SCORE_MODE_MAX_TOKENED_LAST
38+
from cmscommon.constants import \
39+
SCORE_MODE_MAX, SCORE_MODE_MAX_SUBTASK, SCORE_MODE_MAX_TOKENED_LAST
3940
from cmscommon.datetime import make_datetime
4041

4142

@@ -56,18 +57,19 @@ def at(self, timestamp):
5657
def call(self):
5758
return task_score(self.participation, self.task)
5859

59-
def add_result(self, timestamp, score, tokened=False):
60+
def add_result(self, timestamp, score, tokened=False, score_details=None):
61+
score_details = score_details if score_details is not None else []
6062
submission = self.add_submission(
6163
participation=self.participation,
6264
task=self.task,
6365
timestamp=timestamp)
64-
# task_score() only needs score, but all the fields must be set to
65-
# declare the submission result as scored.
66+
# task_score() only needs score and score_details, but all the fields
67+
# must be set to declare the submission result as scored.
6668
self.add_submission_result(submission, self.task.active_dataset,
6769
score=score,
6870
public_score=score,
69-
score_details={},
70-
public_score_details={},
71+
score_details=score_details,
72+
public_score_details=score_details,
7173
ranking_score_details=[])
7274
if tokened:
7375
self.add_token(timestamp=timestamp, submission=submission)
@@ -148,6 +150,104 @@ def test_all_unscored(self):
148150
self.assertEqual(self.call(), (0.0, True))
149151

150152

153+
class TestTaskScoreMaxSubtask(TaskScoreMixin, unittest.TestCase):
154+
"""Tests for task_score() using the max_subtask score mode."""
155+
156+
def setUp(self):
157+
super(TestTaskScoreMaxSubtask, self).setUp()
158+
self.task.score_mode = SCORE_MODE_MAX_SUBTASK
159+
160+
@staticmethod
161+
def subtask(idx, max_score, score_fraction):
162+
"""Return an item of score details for a subtask."""
163+
return {
164+
"idx": idx,
165+
"max_score": max_score,
166+
"score_fraction": score_fraction
167+
}
168+
169+
def test_no_submissions(self):
170+
self.assertEqual(self.call(), (0.0, False))
171+
172+
def test_task_not_group(self):
173+
self.add_result(self.at(1), 66.6, tokened=False)
174+
self.add_result(self.at(2), 44.4, tokened=False)
175+
self.session.flush()
176+
self.assertEqual(self.call(), (66.6, False))
177+
178+
def test_all_submissions_scored(self):
179+
self.add_result(self.at(1), 30 * 0.2 + 40 * 0.5 + 30 * 0.1,
180+
score_details=[
181+
self.subtask(3, 30, 0.2),
182+
self.subtask(2, 40, 0.5),
183+
self.subtask(1, 30, 0.1),
184+
])
185+
self.add_result(self.at(2), 30 * 0.1 + 40 * 0.5 + 30 * 0.2,
186+
score_details=[
187+
self.subtask(2, 40, 0.5),
188+
self.subtask(1, 30, 0.2),
189+
self.subtask(3, 30, 0.1),
190+
])
191+
self.session.flush()
192+
self.assertEqual(self.call(), (30 * 0.2 + 40 * 0.5 + 30 * 0.2, False))
193+
194+
def test_compilation_error_total_is_zero(self):
195+
# Compilation errors have details=[].
196+
self.add_result(self.at(1), 0.0, score_details=[])
197+
self.add_result(self.at(2), 30 * 0.0 + 40 * 0.0 + 30 * 0.0,
198+
score_details=[
199+
self.subtask(3, 30, 0.0),
200+
self.subtask(2, 40, 0.0),
201+
self.subtask(1, 30, 0.0),
202+
])
203+
self.session.flush()
204+
self.assertEqual(self.call(), (30 * 0.0 + 40 * 0.0 + 30 * 0.0, False))
205+
206+
def test_compilation_error_total_is_positive(self):
207+
# Compilation errors have details=[].
208+
self.add_result(self.at(1), 0.0, score_details=[])
209+
self.add_result(self.at(2), 30 * 0.1 + 40 * 0.0 + 30 * 0.0,
210+
score_details=[
211+
self.subtask(3, 30, 0.1),
212+
self.subtask(2, 40, 0.0),
213+
self.subtask(1, 30, 0.0),
214+
])
215+
self.session.flush()
216+
self.assertEqual(self.call(), (30 * 0.1 + 40 * 0.0 + 30 * 0.0, False))
217+
218+
def test_partial(self):
219+
self.add_result(self.at(1), 30 * 0.2 + 40 * 0.5 + 30 * 0.1,
220+
score_details=[
221+
self.subtask(3, 30, 0.2),
222+
self.subtask(2, 40, 0.5),
223+
self.subtask(1, 30, 0.1),
224+
])
225+
self.add_result(self.at(2), 30 * 0.1 + 40 * 0.5 + 30 * 0.2,
226+
score_details=[
227+
self.subtask(3, 30, 0.1),
228+
self.subtask(2, 40, 0.5),
229+
self.subtask(1, 30, 0.2),
230+
])
231+
self.add_result(self.at(3), None)
232+
self.session.flush()
233+
self.assertEqual(self.call(), (30 * 0.2 + 40 * 0.5 + 30 * 0.2, True))
234+
235+
def test_rounding(self):
236+
# No rounding should happen at the subtask or task level.
237+
self.add_result(self.at(1), 80 + 0.0002,
238+
score_details=[
239+
self.subtask(1, 80, 1.0),
240+
self.subtask(2, 20, 0.00001),
241+
])
242+
self.add_result(self.at(2), 0.0004,
243+
score_details=[
244+
self.subtask(1, 80, 0.0),
245+
self.subtask(2, 20, 0.00002),
246+
])
247+
self.session.flush()
248+
self.assertEqual(self.call(), (80 + 0.0004, False))
249+
250+
151251
class TestTaskScoreMax(TaskScoreMixin, unittest.TestCase):
152252
"""Tests for task_score() using the max score mode."""
153253

docs/External contest formats.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ The task YAML files require the following keys.
139139

140140
- ``n_input`` (integer): number of test cases to be evaluated for this task; the actual test cases are retrieved from the :ref:`task directory <externalcontestformats_task-directory>`.
141141

142-
- ``score_mode``: the score mode for the task, as in :ref:`configuringacontest_score`; it can be ``max_tokened_last`` (for the legacy behavior), or ``max`` (for the modern behavior).
142+
- ``score_mode``: the score mode for the task, as in :ref:`configuringacontest_score`; it can be ``max_tokened_last``, ``max``, or ``max_subtask``.
143143

144144
- ``token_mode``: the token mode for the task, as in :ref:`configuringacontest_tokens`; it can be ``disabled``, ``infinite`` or ``finite``; if this is not specified, the loader will try to infer it from the remaining token parameters (in order to retain compatibility with the past), but you are not advised to relay on this behavior.
145145

0 commit comments

Comments
 (0)