feat: diffusion turbulente + recalcul forcé — polygones qui évoluent dans le temps

- DriftSimulationService : ajout diffusion turbulente 600m/h (marche aléatoire
  indépendante par point) → les patches se dispersent et déforment au fil des horizons
  au lieu d'une simple translation rigide avec les alizés constants
- SAMPLE_POINTS 30→40, CONCAVITY 0.8→0.7, MODEL_VERSION 1.0→1.1
- ComputeForecastsCommand : option --force pour supprimer et recalculer les
  forecasts existants (utile après changement de modèle)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gwadaking
2026-04-03 20:03:51 -04:00
parent 492ed02b6f
commit fb25142bad
2 changed files with 49 additions and 13 deletions

View File

@@ -29,32 +29,60 @@ class ComputeForecastsCommand extends Command
protected function configure(): void
{
$this->addOption('limit', 'l', InputOption::VALUE_OPTIONAL,
'Nombre max d\'observations à traiter', 5);
$this
->addOption('limit', 'l', InputOption::VALUE_OPTIONAL,
'Nombre max d\'observations à traiter', 5)
->addOption('force', null, InputOption::VALUE_NONE,
'Supprime les forecasts existants et les recalcule (utile après changement de modèle)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$limit = (int) $input->getOption('limit');
$force = (bool) $input->getOption('force');
$io->title('Calcul forecasts + ImpactScores');
// Observations récentes sans forecast
$observations = $this->em->getRepository(SargassumObservation::class)
// Charge les N observations cibles (avec ou sans forecasts existants)
$qb = $this->em->getRepository(SargassumObservation::class)
->createQueryBuilder('o')
->leftJoin('o.forecasts', 'f')
->where('f.id IS NULL')
->orderBy('o.detectedAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
->setMaxResults($limit);
if (!$force) {
$qb->leftJoin('o.forecasts', 'f')->where('f.id IS NULL');
}
$observations = $qb->getQuery()->getResult();
if (empty($observations)) {
$io->info('Aucune observation sans forecast trouvée.');
return Command::SUCCESS;
}
if ($force) {
$ids = array_map(fn($o) => (string) $o->getId(), $observations);
$io->note(sprintf('--force : recalcul pour %d observation(s)...', count($ids)));
$this->em->getConnection()->executeStatement(
'DELETE FROM sargassum_forecast WHERE source_observation_id IN (:ids)',
['ids' => $ids],
['ids' => \Doctrine\DBAL\ArrayParameterType::STRING]
);
// ImpactScore n'a pas de FK vers l'observation — on purge tout et on recalcule
$this->em->getConnection()->executeStatement('DELETE FROM impact_score');
$this->em->clear();
// Recharge les observations détachées
$observations = $this->em->getRepository(SargassumObservation::class)
->createQueryBuilder('o')
->where('o.id IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->getResult();
}
$io->progressStart(count($observations));
$errors = 0;