505 lines
18 KiB
PHP
505 lines
18 KiB
PHP
<?php
|
|
|
|
namespace Database\Seeders;
|
|
|
|
use App\Jobs\BuildWorkingSetJob;
|
|
use App\Jobs\DuplicateResolutionJob;
|
|
use App\Jobs\MatchQsoGroupJob;
|
|
use App\Jobs\PrepareRunJob;
|
|
use App\Jobs\ScoreGroupJob;
|
|
use App\Jobs\UnpairedClassificationJob;
|
|
use App\Models\Band;
|
|
use App\Models\Category;
|
|
use App\Models\Contest;
|
|
use App\Models\EvaluationRuleSet;
|
|
use App\Models\EvaluationRun;
|
|
use App\Models\Log;
|
|
use App\Models\LogQso;
|
|
use App\Models\PowerCategory;
|
|
use App\Models\QsoResult;
|
|
use App\Models\Round;
|
|
use App\Models\WorkingQso;
|
|
use Illuminate\Database\Seeder;
|
|
use Illuminate\Support\Carbon;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Seeder: EvaluationPipelineRegressionSeeder
|
|
*
|
|
* Vytváří mini dataset a spouští klíčové kroky pipeline synchronně.
|
|
* Slouží jako minimální regresní scénáře bez plného test frameworku.
|
|
*/
|
|
class EvaluationPipelineRegressionSeeder extends Seeder
|
|
{
|
|
public function run(): void
|
|
{
|
|
$band = Band::firstOrCreate(
|
|
['name' => '145 MHz'],
|
|
[
|
|
'order' => 1,
|
|
'edi_band_begin' => 144.0,
|
|
'edi_band_end' => 146.0,
|
|
'has_power_category' => true,
|
|
]
|
|
);
|
|
$category = Category::firstOrCreate(
|
|
['name' => 'SINGLE'],
|
|
[
|
|
'order' => 1,
|
|
]
|
|
);
|
|
$power = PowerCategory::firstOrCreate(
|
|
['name' => 'LP'],
|
|
[
|
|
'order' => 1,
|
|
'power_level' => 100,
|
|
]
|
|
);
|
|
|
|
$ruleSetUnique = EvaluationRuleSet::firstOrCreate(
|
|
['code' => 'REG_UNIQUE'],
|
|
[
|
|
'name' => 'Regression UNIQUE',
|
|
'scoring_mode' => 'FIXED_POINTS',
|
|
'points_per_qso' => 1,
|
|
'use_multipliers' => false,
|
|
'dup_qso_policy' => 'ZERO_POINTS',
|
|
'nil_qso_policy' => 'ZERO_POINTS',
|
|
'busted_call_policy' => 'ZERO_POINTS',
|
|
'busted_exchange_policy' => 'ZERO_POINTS',
|
|
'time_tolerance_sec' => 60,
|
|
'require_unique_qso' => true,
|
|
]
|
|
);
|
|
|
|
$ruleSetNoUnique = EvaluationRuleSet::firstOrCreate(
|
|
['code' => 'REG_DUP'],
|
|
[
|
|
'name' => 'Regression DUP',
|
|
'scoring_mode' => 'FIXED_POINTS',
|
|
'points_per_qso' => 1,
|
|
'use_multipliers' => false,
|
|
'dup_qso_policy' => 'ZERO_POINTS',
|
|
'nil_qso_policy' => 'ZERO_POINTS',
|
|
'busted_call_policy' => 'ZERO_POINTS',
|
|
'busted_exchange_policy' => 'ZERO_POINTS',
|
|
'time_tolerance_sec' => 60,
|
|
'require_unique_qso' => false,
|
|
]
|
|
);
|
|
|
|
$ruleSetTimeMismatch = EvaluationRuleSet::firstOrCreate(
|
|
['code' => 'REG_TIME_MISMATCH'],
|
|
[
|
|
'name' => 'Regression TIME_MISMATCH',
|
|
'scoring_mode' => 'FIXED_POINTS',
|
|
'points_per_qso' => 1,
|
|
'use_multipliers' => false,
|
|
'dup_qso_policy' => 'ZERO_POINTS',
|
|
'nil_qso_policy' => 'ZERO_POINTS',
|
|
'busted_call_policy' => 'ZERO_POINTS',
|
|
'busted_exchange_policy' => 'ZERO_POINTS',
|
|
'time_tolerance_sec' => 60,
|
|
'allow_time_mismatch_pairing' => true,
|
|
'time_mismatch_max_sec' => 600,
|
|
'time_mismatch_policy' => 'ZERO_POINTS',
|
|
]
|
|
);
|
|
$ruleSetFlags = EvaluationRuleSet::firstOrCreate(
|
|
['code' => 'REG_FLAGS'],
|
|
[
|
|
'name' => 'Regression FLAGS',
|
|
'scoring_mode' => 'FIXED_POINTS',
|
|
'points_per_qso' => 1,
|
|
'use_multipliers' => false,
|
|
'dup_qso_policy' => 'ZERO_POINTS',
|
|
'nil_qso_policy' => 'ZERO_POINTS',
|
|
'busted_call_policy' => 'ZERO_POINTS',
|
|
'busted_exchange_policy' => 'ZERO_POINTS',
|
|
'exchange_requires_report' => true,
|
|
'exchange_requires_serial' => true,
|
|
'rst_ignore_third_char' => true,
|
|
'ignore_slash_part' => true,
|
|
'callsign_suffix_max_len' => 4,
|
|
'callsign_levenshtein_max' => 1,
|
|
'discard_qso_rec_diff_rst' => true,
|
|
'discard_qso_rec_diff_serial' => true,
|
|
'time_tolerance_sec' => 60,
|
|
]
|
|
);
|
|
|
|
$contest = Contest::create([
|
|
'name' => ['cs' => 'Regression Contest', 'en' => 'Regression Contest'],
|
|
'description' => ['cs' => 'Regresní sada', 'en' => 'Regression set'],
|
|
'is_active' => false,
|
|
'is_test' => true,
|
|
'rule_set_id' => $ruleSetUnique->id,
|
|
]);
|
|
$contest->bands()->sync([$band->id]);
|
|
$contest->categories()->sync([$category->id]);
|
|
$contest->powerCategories()->sync([$power->id]);
|
|
|
|
$this->scenarioUniqueGlobal($contest, $band, $category, $power, $ruleSetUnique);
|
|
$this->scenarioUniqueNotGlobal($contest, $band, $category, $power, $ruleSetUnique);
|
|
$this->scenarioDuplicateResolution($contest, $band, $category, $power, $ruleSetNoUnique);
|
|
$this->scenarioTimeMismatchPolicy($contest, $band, $category, $power, $ruleSetTimeMismatch);
|
|
$this->scenarioFlagCoverage($contest, $band, $category, $power, $ruleSetFlags);
|
|
}
|
|
|
|
private function scenarioUniqueGlobal(
|
|
Contest $contest,
|
|
Band $band,
|
|
Category $category,
|
|
PowerCategory $power,
|
|
EvaluationRuleSet $ruleSet
|
|
): void {
|
|
$round = $this->makeRound($contest, $ruleSet, 'UNIQUE global');
|
|
$this->attachRoundScopes($round, $band, $category, $power);
|
|
|
|
$log = $this->makeLog($round, 'OK1AAA', 'JN79AB', '145');
|
|
$qso = LogQso::create([
|
|
'log_id' => $log->id,
|
|
'qso_index' => 1,
|
|
'time_on' => Carbon::parse($round->start_time)->addMinutes(5),
|
|
'freq_khz' => 144000,
|
|
'my_call' => 'OK1AAA',
|
|
'dx_call' => 'UNIQUE1',
|
|
'my_locator' => 'JN79AB',
|
|
'rx_wwl' => 'JN79AB',
|
|
]);
|
|
|
|
$run = $this->makeRun($round, $ruleSet);
|
|
$this->runPipeline($run);
|
|
|
|
$errorCode = QsoResult::where('evaluation_run_id', $run->id)
|
|
->where('log_qso_id', $qso->id)
|
|
->value('error_code');
|
|
if ($errorCode !== 'UNIQUE') {
|
|
throw new RuntimeException("Regression UNIQUE global selhal: očekáván UNIQUE, got {$errorCode}.");
|
|
}
|
|
}
|
|
|
|
private function scenarioUniqueNotGlobal(
|
|
Contest $contest,
|
|
Band $band,
|
|
Category $category,
|
|
PowerCategory $power,
|
|
EvaluationRuleSet $ruleSet
|
|
): void {
|
|
$round = $this->makeRound($contest, $ruleSet, 'UNIQUE not global');
|
|
$this->attachRoundScopes($round, $band, $category, $power);
|
|
|
|
$logA = $this->makeLog($round, 'OK1AAB', 'JN79AB', '145');
|
|
$logB = $this->makeLog($round, 'OK1AAC', 'JN79AB', '145');
|
|
$qsoA = LogQso::create([
|
|
'log_id' => $logA->id,
|
|
'qso_index' => 1,
|
|
'time_on' => Carbon::parse($round->start_time)->addMinutes(5),
|
|
'freq_khz' => 144000,
|
|
'my_call' => 'OK1AAB',
|
|
'dx_call' => 'X2AAA',
|
|
'my_locator' => 'JN79AB',
|
|
'rx_wwl' => 'JN79AB',
|
|
]);
|
|
LogQso::create([
|
|
'log_id' => $logB->id,
|
|
'qso_index' => 1,
|
|
'time_on' => Carbon::parse($round->start_time)->addMinutes(6),
|
|
'freq_khz' => 144000,
|
|
'my_call' => 'OK1AAC',
|
|
'dx_call' => 'X2AAA',
|
|
'my_locator' => 'JN79AB',
|
|
'rx_wwl' => 'JN79AB',
|
|
]);
|
|
|
|
$run = $this->makeRun($round, $ruleSet);
|
|
$this->runPipeline($run);
|
|
|
|
$errorCode = QsoResult::where('evaluation_run_id', $run->id)
|
|
->where('log_qso_id', $qsoA->id)
|
|
->value('error_code');
|
|
if ($errorCode === 'UNIQUE') {
|
|
throw new RuntimeException('Regression UNIQUE not global selhal: UNIQUE nesmí vzniknout.');
|
|
}
|
|
}
|
|
|
|
private function scenarioDuplicateResolution(
|
|
Contest $contest,
|
|
Band $band,
|
|
Category $category,
|
|
PowerCategory $power,
|
|
EvaluationRuleSet $ruleSet
|
|
): void {
|
|
$round = $this->makeRound($contest, $ruleSet, 'DUP resolution');
|
|
$this->attachRoundScopes($round, $band, $category, $power);
|
|
|
|
$log = $this->makeLog($round, 'OK1DUP', 'JN79AB', '145');
|
|
LogQso::create([
|
|
'log_id' => $log->id,
|
|
'qso_index' => 1,
|
|
'time_on' => Carbon::parse($round->start_time)->addMinutes(5),
|
|
'freq_khz' => 144000,
|
|
'my_call' => 'OK1DUP',
|
|
'dx_call' => 'DUP1',
|
|
'my_locator' => 'JN79AB',
|
|
'rx_wwl' => 'JN79AB',
|
|
]);
|
|
LogQso::create([
|
|
'log_id' => $log->id,
|
|
'qso_index' => 2,
|
|
'time_on' => Carbon::parse($round->start_time)->addMinutes(6),
|
|
'freq_khz' => 144000,
|
|
'my_call' => 'OK1DUP',
|
|
'dx_call' => 'DUP1',
|
|
'my_locator' => 'JN79AB',
|
|
'rx_wwl' => 'JN79AB',
|
|
]);
|
|
|
|
$run = $this->makeRun($round, $ruleSet);
|
|
$this->runPipeline($run);
|
|
|
|
$dupCount = QsoResult::where('evaluation_run_id', $run->id)
|
|
->where('error_code', 'DUP')
|
|
->count();
|
|
if ($dupCount !== 1) {
|
|
throw new RuntimeException("Regression DUP selhal: očekáván 1 DUP, got {$dupCount}.");
|
|
}
|
|
}
|
|
|
|
private function scenarioTimeMismatchPolicy(
|
|
Contest $contest,
|
|
Band $band,
|
|
Category $category,
|
|
PowerCategory $power,
|
|
EvaluationRuleSet $ruleSet
|
|
): void {
|
|
$round = $this->makeRound($contest, $ruleSet, 'TIME_MISMATCH policy');
|
|
$this->attachRoundScopes($round, $band, $category, $power);
|
|
|
|
$logA = $this->makeLog($round, 'OK1TMA', 'JN79AB', '145');
|
|
$logB = $this->makeLog($round, 'OK1TMB', 'JN79AB', '145');
|
|
|
|
$timeA = Carbon::parse($round->start_time)->addMinutes(5);
|
|
$timeB = Carbon::parse($round->start_time)->addMinutes(9);
|
|
|
|
$qsoA = LogQso::create([
|
|
'log_id' => $logA->id,
|
|
'qso_index' => 1,
|
|
'time_on' => $timeA,
|
|
'freq_khz' => 144000,
|
|
'my_call' => 'OK1TMA',
|
|
'dx_call' => 'OK1TMB',
|
|
'my_serial' => '001',
|
|
'dx_serial' => '002',
|
|
'my_locator' => 'JN79AB',
|
|
'rx_wwl' => 'JN79AB',
|
|
]);
|
|
LogQso::create([
|
|
'log_id' => $logB->id,
|
|
'qso_index' => 1,
|
|
'time_on' => $timeB,
|
|
'freq_khz' => 144000,
|
|
'my_call' => 'OK1TMB',
|
|
'dx_call' => 'OK1TMA',
|
|
'my_serial' => '002',
|
|
'dx_serial' => '001',
|
|
'my_locator' => 'JN79AB',
|
|
'rx_wwl' => 'JN79AB',
|
|
]);
|
|
|
|
$run = $this->makeRun($round, $ruleSet);
|
|
$this->runPipeline($run);
|
|
|
|
$result = QsoResult::where('evaluation_run_id', $run->id)
|
|
->where('log_qso_id', $qsoA->id)
|
|
->first();
|
|
if (! $result || $result->error_code !== 'TIME_MISMATCH') {
|
|
throw new RuntimeException('Regression TIME_MISMATCH selhal: očekáván TIME_MISMATCH.');
|
|
}
|
|
if (! $result->is_valid || (int) $result->points !== 0) {
|
|
throw new RuntimeException('Regression TIME_MISMATCH selhal: očekáván validní QSO s 0 body.');
|
|
}
|
|
}
|
|
|
|
private function scenarioFlagCoverage(
|
|
Contest $contest,
|
|
Band $band,
|
|
Category $category,
|
|
PowerCategory $power,
|
|
EvaluationRuleSet $ruleSet
|
|
): void {
|
|
$round = $this->makeRound($contest, $ruleSet, 'FLAGS coverage');
|
|
$this->attachRoundScopes($round, $band, $category, $power);
|
|
|
|
$logSuffix = $this->makeLog($round, 'OK1AAA/P', 'JN79AB', '145');
|
|
$qsoFuzzySource = LogQso::create([
|
|
'log_id' => $logSuffix->id,
|
|
'qso_index' => 1,
|
|
'time_on' => Carbon::parse($round->start_time)->addMinutes(2),
|
|
'freq_khz' => 144000,
|
|
'my_call' => 'OK1AAA/P',
|
|
'dx_call' => 'OK1AAB',
|
|
'my_rst' => '599',
|
|
'dx_rst' => '599',
|
|
'my_serial' => '001',
|
|
'dx_serial' => '002',
|
|
'my_locator' => 'JN79AB',
|
|
'rx_wwl' => 'JN79AB',
|
|
]);
|
|
|
|
$logFuzzy = $this->makeLog($round, 'OK1AAB', 'JN79AB', '145');
|
|
$qsoFuzzy = LogQso::create([
|
|
'log_id' => $logFuzzy->id,
|
|
'qso_index' => 1,
|
|
'time_on' => Carbon::parse($round->start_time)->addMinutes(2),
|
|
'freq_khz' => 144000,
|
|
'my_call' => 'OK1AAB',
|
|
'dx_call' => 'OK1AAX',
|
|
'my_rst' => '599',
|
|
'dx_rst' => '599',
|
|
'my_serial' => '002',
|
|
'dx_serial' => '001',
|
|
'my_locator' => 'JN79AB',
|
|
'rx_wwl' => 'JN79AB',
|
|
]);
|
|
$logSerial = $this->makeLog($round, 'OK1SER', 'JN79AB', '145');
|
|
$qsoSerialMismatch = LogQso::create([
|
|
'log_id' => $logSuffix->id,
|
|
'qso_index' => 2,
|
|
'time_on' => Carbon::parse($round->start_time)->addMinutes(3),
|
|
'freq_khz' => 144000,
|
|
'my_call' => 'OK1AAA/P',
|
|
'dx_call' => 'OK1SER',
|
|
'my_rst' => '599',
|
|
'dx_rst' => '599',
|
|
'my_serial' => '005',
|
|
'dx_serial' => '006',
|
|
'my_locator' => 'JN79AB',
|
|
'rx_wwl' => 'JN79AB',
|
|
]);
|
|
LogQso::create([
|
|
'log_id' => $logSerial->id,
|
|
'qso_index' => 1,
|
|
'time_on' => Carbon::parse($round->start_time)->addMinutes(3),
|
|
'freq_khz' => 144000,
|
|
'my_call' => 'OK1SER',
|
|
'dx_call' => 'OK1AAA',
|
|
'my_rst' => '599',
|
|
'dx_rst' => '599',
|
|
'my_serial' => '007',
|
|
'dx_serial' => '008',
|
|
'my_locator' => 'JN79AB',
|
|
'rx_wwl' => 'JN79AB',
|
|
]);
|
|
|
|
$run = $this->makeRun($round, $ruleSet);
|
|
$this->runPipeline($run);
|
|
|
|
$suffixNorm = \App\Models\WorkingQso::where('evaluation_run_id', $run->id)
|
|
->where('log_id', $logSuffix->id)
|
|
->value('call_norm');
|
|
if ($suffixNorm !== 'OK1AAA') {
|
|
throw new RuntimeException('Regression FLAGS selhal: callsign suffix nebyl normalizován.');
|
|
}
|
|
|
|
$fuzzyResult = QsoResult::where('evaluation_run_id', $run->id)
|
|
->where('log_qso_id', $qsoFuzzy->id)
|
|
->first();
|
|
if (! $fuzzyResult || $fuzzyResult->match_type !== 'MATCH_FUZZY_CALL_1') {
|
|
throw new RuntimeException('Regression FLAGS selhal: callsign_levenshtein_max neovlivňuje matching.');
|
|
}
|
|
|
|
if ($fuzzyResult->error_code === 'BUSTED_RST') {
|
|
throw new RuntimeException('Regression FLAGS selhal: rst_ignore_third_char nebylo zohledněno.');
|
|
}
|
|
|
|
$serialResult = QsoResult::where('evaluation_run_id', $run->id)
|
|
->where('log_qso_id', $qsoSerialMismatch->id)
|
|
->where('error_code', 'BUSTED_SERIAL')
|
|
->first();
|
|
if (! $serialResult) {
|
|
throw new RuntimeException('Regression FLAGS selhal: discard_qso_rec_diff_serial nebylo zohledněno.');
|
|
}
|
|
}
|
|
|
|
private function runPipeline(EvaluationRun $run): void
|
|
{
|
|
(new PrepareRunJob($run->id))->handle();
|
|
(new BuildWorkingSetJob($run->id))->handle();
|
|
|
|
$run = EvaluationRun::find($run->id);
|
|
$group = $run && isset($run->scope['groups'][0])
|
|
? $run->scope['groups'][0]
|
|
: [
|
|
'key' => 'b0',
|
|
'band_id' => null,
|
|
'category_id' => null,
|
|
'power_category_id' => null,
|
|
];
|
|
if (! ($group['band_id'] ?? null)) {
|
|
$workingBandId = WorkingQso::where('evaluation_run_id', $run->id)
|
|
->whereNotNull('band_id')
|
|
->value('band_id');
|
|
if ($workingBandId) {
|
|
$group['band_id'] = (int) $workingBandId;
|
|
$group['key'] = 'b' . $workingBandId;
|
|
}
|
|
}
|
|
$groupKey = $group['key'] ?? 'b0';
|
|
|
|
(new MatchQsoGroupJob($run->id, $groupKey, $group, 1))->handle();
|
|
(new MatchQsoGroupJob($run->id, $groupKey, $group, 2))->handle();
|
|
(new UnpairedClassificationJob($run->id))->handle();
|
|
(new DuplicateResolutionJob($run->id))->handle();
|
|
(new ScoreGroupJob($run->id, $groupKey, $group))->handle();
|
|
}
|
|
|
|
private function makeRound(Contest $contest, EvaluationRuleSet $ruleSet, string $label): Round
|
|
{
|
|
$start = now()->subDays(1);
|
|
$end = now()->subDays(1)->addHours(5);
|
|
|
|
return Round::create([
|
|
'contest_id' => $contest->id,
|
|
'rule_set_id' => $ruleSet->id,
|
|
'name' => ['cs' => "Regrese {$label}", 'en' => "Regression {$label}"],
|
|
'description' => ['cs' => 'Regresní běh', 'en' => 'Regression run'],
|
|
'is_test' => true,
|
|
'is_active' => false,
|
|
'start_time' => $start,
|
|
'end_time' => $end,
|
|
'logs_deadline' => now(),
|
|
]);
|
|
}
|
|
|
|
private function attachRoundScopes(Round $round, Band $band, Category $category, PowerCategory $power): void
|
|
{
|
|
$round->bands()->sync([$band->id]);
|
|
$round->categories()->sync([$category->id]);
|
|
$round->powerCategories()->sync([$power->id]);
|
|
}
|
|
|
|
private function makeLog(Round $round, string $pcall, string $pwwlo, string $pband): Log
|
|
{
|
|
return Log::create([
|
|
'round_id' => $round->id,
|
|
'pcall' => $pcall,
|
|
'pwwlo' => $pwwlo,
|
|
'pband' => $pband,
|
|
'power_category_id' => PowerCategory::first()?->id,
|
|
]);
|
|
}
|
|
|
|
private function makeRun(Round $round, EvaluationRuleSet $ruleSet): EvaluationRun
|
|
{
|
|
return EvaluationRun::create([
|
|
'round_id' => $round->id,
|
|
'rule_set_id' => $ruleSet->id,
|
|
'status' => 'PENDING',
|
|
'current_step' => 'seed',
|
|
'is_official' => false,
|
|
'result_type' => 'TEST',
|
|
]);
|
|
}
|
|
}
|