Skip to content

Basic implementation of partial scoring. #3047

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions judge/judgedaemon.main.php
Original file line number Diff line number Diff line change
Expand Up @@ -1461,6 +1461,7 @@ function judge(array $judgeTask): bool
}
}

$compare_args = $compare_config['compare_args'];
$test_run_cmd = LIBJUDGEDIR . "/testcase_run.sh $cpuset_opt " .
implode(' ', array_map('dj_escapeshellarg', [
$input,
Expand All @@ -1469,7 +1470,7 @@ function judge(array $judgeTask): bool
$passdir,
$run_runpath,
$compare_runpath,
$compare_config['compare_args']
$compare_args,
]));
system($test_run_cmd, $retval);

Expand Down Expand Up @@ -1523,11 +1524,16 @@ function judge(array $judgeTask): bool
if (file_exists($passdir . '/feedback/teammessage.txt')) {
$new_judging_run['team_message'] = rest_encode_file($passdir . '/feedback/teammessage.txt', $output_storage_limit);
}
$score = "";
if ($result === 'correct' && file_exists($passdir . '/feedback/score.txt')) {
$new_judging_run['score'] = rest_encode_file($passdir . '/feedback/score.txt');
$score = ", score: " . trim(dj_file_get_contents($passdir . '/feedback/score.txt'));
}

if ($passLimit > 1) {
$walltime = $metadata['wall-time'] ?? '?';
logmsg(LOG_INFO, ' ' . ($result === 'correct' ? " \033[0;32m✔\033[0m" : " \033[1;31m✗\033[0m")
. ' ...done in ' . $walltime . 's (CPU: ' . $runtime . 's), result: ' . $result);
. ' ...done in ' . $walltime . 's (CPU: ' . $runtime . 's), result: ' . $result . $score);
}

if ($result !== 'correct') {
Expand Down Expand Up @@ -1575,7 +1581,7 @@ function judge(array $judgeTask): bool
if ($passLimit == 1) {
$walltime = $metadata['wall-time'] ?? '?';
logmsg(LOG_INFO, ' ' . ($result === 'correct' ? " \033[0;32m✔\033[0m" : " \033[1;31m✗\033[0m")
. ' ...done in ' . $walltime . 's (CPU: ' . $runtime . 's), result: ' . $result);
. ' ...done in ' . $walltime . 's (CPU: ' . $runtime . 's), result: ' . $result . $score);
}

// done!
Expand Down
66 changes: 66 additions & 0 deletions webapp/migrations/Version20250704181912.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250704181912 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add basic support for partial scoring';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE testcase_group (testcase_group_id INT UNSIGNED AUTO_INCREMENT NOT NULL COMMENT \'Testcase group ID\', name VARCHAR(255) NOT NULL COMMENT \'Name of the testcase group\', accept_score VARCHAR(255) DEFAULT NULL COMMENT \'Score if this group is accepted\', range_lower_bound VARCHAR(255) DEFAULT NULL COMMENT \'Lower bound of the score range\', range_upper_bound VARCHAR(255) DEFAULT NULL COMMENT \'Upper bound of the score range\', aggregation_type VARCHAR(255) DEFAULT \'sum\' NOT NULL COMMENT \'How to aggregate scores for this group\', ignore_sample TINYINT(1) DEFAULT 0 NOT NULL COMMENT \'Ignore the sample testcases when aggregating scores\', PRIMARY KEY(testcase_group_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'Testcase group metadata\' ');
$this->addSql('ALTER TABLE testcase ADD testcase_group_id INT UNSIGNED DEFAULT NULL COMMENT \'Testcase group ID\'');
$this->addSql('ALTER TABLE testcase ADD CONSTRAINT FK_4C1E5C391FF421A3 FOREIGN KEY (testcase_group_id) REFERENCES testcase_group (testcase_group_id) ON DELETE SET NULL');
$this->addSql('CREATE INDEX IDX_4C1E5C391FF421A3 ON testcase (testcase_group_id)');
$this->addSql('ALTER TABLE judging_run ADD score NUMERIC(32, 9) DEFAULT \'0.000000000\' NOT NULL COMMENT \'Optional score for this run, e.g. for partial scoring\'');
$this->addSql('ALTER TABLE judging ADD score NUMERIC(32, 9) DEFAULT \'0.000000000\' NOT NULL COMMENT \'Optional score for this run, e.g. for partial scoring\'');
$this->addSql('ALTER TABLE testcase_group ADD parent_id INT UNSIGNED DEFAULT NULL COMMENT \'Testcase group ID\'');
$this->addSql('ALTER TABLE testcase_group ADD CONSTRAINT FK_F02888FE727ACA70 FOREIGN KEY (parent_id) REFERENCES testcase_group (testcase_group_id) ON DELETE SET NULL');
$this->addSql('CREATE INDEX IDX_F02888FE727ACA70 ON testcase_group (parent_id)');
$this->addSql('ALTER TABLE problem ADD parent_testcase_group_id INT UNSIGNED DEFAULT NULL COMMENT \'Testcase group ID\'');
$this->addSql('ALTER TABLE problem ADD CONSTRAINT FK_D7E7CCC8A090DCC7 FOREIGN KEY (parent_testcase_group_id) REFERENCES testcase_group (testcase_group_id) ON DELETE SET NULL');
$this->addSql('CREATE INDEX IDX_D7E7CCC8A090DCC7 ON problem (parent_testcase_group_id)');
$this->addSql('ALTER TABLE testcase_group ADD output_validator_flags VARCHAR(255) DEFAULT NULL COMMENT \'Flags for output validation\'');
$this->addSql('ALTER TABLE testcase_group ADD on_reject_continue TINYINT(1) DEFAULT 0 NOT NULL COMMENT \'Continue on reject\'');
$this->addSql('ALTER TABLE scorecache ADD score_public NUMERIC(32, 9) DEFAULT \'0.000000000\' NOT NULL COMMENT \'Optional score for this run, e.g. for partial scoring\', ADD score_restricted NUMERIC(32, 9) DEFAULT \'0.000000000\' NOT NULL COMMENT \'Optional score for this run, e.g. for partial scoring (for restricted audience)\'');
$this->addSql('ALTER TABLE rankcache ADD score_public NUMERIC(32, 9) DEFAULT \'0.000000000\' NOT NULL COMMENT \'Optional score for this run, e.g. for partial scoring\', ADD score_restricted NUMERIC(32, 9) DEFAULT \'0.000000000\' NOT NULL COMMENT \'Optional score for this run, e.g. for partial scoring (for restricted audience)\'');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE rankcache DROP score_public, DROP score_restricted');
$this->addSql('ALTER TABLE scorecache DROP score_public, DROP score_restricted');
$this->addSql('ALTER TABLE testcase_group DROP on_reject_continue');
$this->addSql('ALTER TABLE testcase_group DROP output_validator_flags');
$this->addSql('ALTER TABLE problem DROP FOREIGN KEY FK_D7E7CCC8A090DCC7');
$this->addSql('DROP INDEX IDX_D7E7CCC8A090DCC7 ON problem');
$this->addSql('ALTER TABLE problem DROP parent_testcase_group_id');
$this->addSql('ALTER TABLE testcase_group DROP FOREIGN KEY FK_F02888FE727ACA70');
$this->addSql('DROP INDEX IDX_F02888FE727ACA70 ON testcase_group');
$this->addSql('ALTER TABLE testcase_group DROP parent_id');
$this->addSql('ALTER TABLE judging DROP score');
$this->addSql('ALTER TABLE judging_run DROP score');
$this->addSql('ALTER TABLE testcase DROP FOREIGN KEY FK_4C1E5C391FF421A3');
$this->addSql('DROP TABLE testcase_group');
$this->addSql('DROP INDEX IDX_4C1E5C391FF421A3 ON testcase');
$this->addSql('ALTER TABLE testcase DROP testcase_group_id');
}

public function isTransactional(): bool
{
return false;
}
}
36 changes: 36 additions & 0 deletions webapp/migrations/Version20250804192300.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250804192300 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE external_judgement ADD score NUMERIC(32, 9) DEFAULT \'0.000000000\' NOT NULL COMMENT \'Optional score for this run, e.g. for partial scoring\'');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE external_judgement DROP score');
}

public function isTransactional(): bool
{
return false;
}
}
36 changes: 36 additions & 0 deletions webapp/migrations/Version20250804194436.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250804194436 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE external_run ADD score NUMERIC(32, 9) DEFAULT \'0.000000000\' NOT NULL COMMENT \'Optional score for this run, e.g. for partial scoring\'');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE external_run DROP score');
}

public function isTransactional(): bool
{
return false;
}
}
41 changes: 32 additions & 9 deletions webapp/src/Controller/API/JudgehostController.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use App\Entity\Rejudging;
use App\Entity\Submission;
use App\Entity\SubmissionFile;
use App\Entity\TestcaseAggregationType;
use App\Entity\TestcaseContent;
use App\Entity\Version;
use App\Service\BalloonService;
Expand Down Expand Up @@ -652,15 +653,16 @@ public function addJudgingRunAction(
$teamMessage = $request->request->get('team_message');
$metadata = $request->request->get('metadata');
$testcasedir = $request->request->get('testcasedir');
$compareMeta = $request->request->get('compare_metadata');
$compareMeta = $request->request->get('compare_metadata');
$score = $request->request->get('score');

$judgehost = $this->em->getRepository(Judgehost::class)->findOneBy(['hostname' => $hostname]);
if (!$judgehost) {
throw new BadRequestHttpException("Who are you and why are you sending us any data?");
}

$hasFinalResult = $this->addSingleJudgingRun($judgeTaskId, $hostname, $runResult, $runTime,
$outputSystem, $outputError, $outputDiff, $outputRun, $teamMessage, $metadata, $testcasedir, $compareMeta);
$outputSystem, $outputError, $outputDiff, $outputRun, $teamMessage, $metadata, $testcasedir, $compareMeta, $score);
$judgehost = $this->em->getRepository(Judgehost::class)->findOneBy(['hostname' => $hostname]);
$judgehost->setPolltime(Utils::now());
$this->em->flush();
Expand Down Expand Up @@ -932,6 +934,7 @@ private function addSingleJudgingRun(
string $metadata,
?string $testcasedir,
?string $compareMeta,
?string $score = null
): bool {
$resultsRemap = $this->config->get('results_remap');
$resultsPrio = $this->config->get('results_prio');
Expand All @@ -953,7 +956,8 @@ private function addSingleJudgingRun(
$teamMessage,
$metadata,
$testcasedir,
$compareMeta
$compareMeta,
$score
) {
$judgingRun = $this->em->getRepository(JudgingRun::class)->findOneBy(
['judgetaskid' => $judgeTaskId]);
Expand Down Expand Up @@ -983,6 +987,10 @@ private function addSingleJudgingRun(
$judgingRunOutput->setTeamMessage(base64_decode($teamMessage));
}

if ($score) {
$judgingRun->setScore(base64_decode($score));
}

$judging = $judgingRun->getJudging();
$this->maybeUpdateActiveJudging($judging);
$this->em->flush();
Expand Down Expand Up @@ -1019,15 +1027,30 @@ private function addSingleJudgingRun(
$oldResult = $judging->getResult();

$lazyEval = DOMJudgeService::EVAL_LAZY;
if (($result = SubmissionService::getFinalResult($runresults, $resultsPrio)) !== null) {
$problem = $judging->getSubmission()->getProblem();
if ($problem->isScoringProblem()) {
$parentGroup = $problem->getParentTestcaseGroup();
$scoreAndResult = SubmissionService::maybeSetScoringResult(
$parentGroup,
$judging
);
$score = $scoreAndResult[0];
$result = $scoreAndResult[1];
} else {
$result = SubmissionService::getFinalResult($runresults, $resultsPrio);
}
if ($result !== null) {
// Lookup global lazy evaluation of results setting and possible problem specific override.
$lazyEval = $this->config->get('lazy_eval_results');
$lazyEval = $this->config->get('lazy_eval_results');
$problemLazy = $judging->getSubmission()->getContestProblem()->getLazyEvalResults();
if ($problemLazy !== DOMJudgeService::EVAL_DEFAULT) {
$lazyEval = $problemLazy;
}

$judging->setResult($result);
if ($problem->isScoringProblem()) {
$judging->setScore($score);
}

$hasNullResults = false;
foreach ($runresults as $runresult) {
Expand Down Expand Up @@ -1100,16 +1123,16 @@ private function addSingleJudgingRun(
}

$submission = $judging->getSubmission();
$contest = $submission->getContest();
$team = $submission->getTeam();
$problem = $submission->getProblem();
$contest = $submission->getContest();
$team = $submission->getTeam();
$problem = $submission->getProblem();
$this->scoreboardService->calculateScoreRow($contest, $team, $problem);

// We call alert here before possible validation. Note that
// this means that these alert messages should be treated as
// confidential information.
$msg = sprintf("submission %s, judging %s: %s",
$submission->getSubmitid(), $judging->getJudgingid(), $result);
$submission->getSubmitid(), $judging->getJudgingid(), $result);
$this->dj->alert($result === 'correct' ? 'accept' : 'reject', $msg);

// Potentially send a balloon, i.e. if no verification required (case of verification required is
Expand Down
1 change: 1 addition & 0 deletions webapp/src/Controller/BaseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use App\Entity\ScoreCache;
use App\Entity\Team;
use App\Entity\TeamCategory;
use App\Entity\TestcaseAggregationType;
use App\Service\DOMJudgeService;
use App\Service\EventLogService;
use App\Utils\Utils;
Expand Down
1 change: 1 addition & 0 deletions webapp/src/Controller/Jury/ImportExportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ public function indexAction(Request $request): Response
try {
$zip = $this->dj->openZipFile($archive->getRealPath());
$clientName = $archive->getClientOriginalName();
/** @var array<string, string[]> $messages */
$messages = [];
if ($contestId === null) {
$contest = null;
Expand Down
1 change: 1 addition & 0 deletions webapp/src/Controller/Jury/ProblemController.php
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,7 @@ public function editAction(Request $request, int $probId): Response
$data = $uploadForm->getData();
/** @var UploadedFile $archive */
$archive = $data['archive'];
/** @var array<string, string[]> $messages */
$messages = [];

/** @var Contest|null $contest */
Expand Down
1 change: 1 addition & 0 deletions webapp/src/DataTransferObject/Shadowing/JudgementEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ public function __construct(
public readonly string $submissionId,
public readonly ?string $endTime,
public readonly ?string $judgementTypeId,
public readonly string|float|null $score,
) {}
}
1 change: 1 addition & 0 deletions webapp/src/DataTransferObject/Shadowing/RunEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ public function __construct(
public readonly ?string $judgementTypeId,
public readonly ?string $time,
public readonly ?float $runTime,
public readonly string|float|null $score,
) {}
}
22 changes: 22 additions & 0 deletions webapp/src/Entity/ExternalJudgement.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ class ExternalJudgement
)]
private bool $valid = true;

#[ORM\Column(
type: 'decimal',
precision: 32,
scale: 9,
options: [
'comment' => 'Optional score for this run, e.g. for partial scoring',
'default' => '0.000000000',
]
)]
private string|float $score = 0;

#[ORM\ManyToOne]
#[ORM\JoinColumn(name: 'cid', referencedColumnName: 'cid', onDelete: 'CASCADE')]
private Contest $contest;
Expand Down Expand Up @@ -249,4 +260,15 @@ public function getSumRuntime(): float
}
return $sum;
}

public function getScore(): string|float
{
return $this->score;
}

public function setScore(string|float $score): ExternalJudgement
{
$this->score = $score;
return $this;
}
}
11 changes: 11 additions & 0 deletions webapp/src/Entity/ExternalRun.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ class ExternalRun
#[ORM\JoinColumn(name: 'cid', referencedColumnName: 'cid', onDelete: 'CASCADE')]
private Contest $contest;

#[ORM\Column(
type: 'decimal',
precision: 32,
scale: 9,
options: [
'comment' => 'Optional score for this run, e.g. for partial scoring',
'default' => '0.000000000',
]
)]
private string|float $score = 0;

public function getExtrunid(): int
{
return $this->extrunid;
Expand Down
Loading
Loading