diff --git a/backend/src/Command/ComputeForecastsCommand.php b/backend/src/Command/ComputeForecastsCommand.php index 17d53ee..7c37f6a 100644 --- a/backend/src/Command/ComputeForecastsCommand.php +++ b/backend/src/Command/ComputeForecastsCommand.php @@ -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; diff --git a/backend/src/Service/Forecast/DriftSimulationService.php b/backend/src/Service/Forecast/DriftSimulationService.php index e23ffd3..aca8883 100644 --- a/backend/src/Service/Forecast/DriftSimulationService.php +++ b/backend/src/Service/Forecast/DriftSimulationService.php @@ -22,10 +22,11 @@ use Psr\Log\LoggerInterface; class DriftSimulationService { private const HORIZONS = [6, 12, 24, 48]; - private const SAMPLE_POINTS = 30; + private const SAMPLE_POINTS = 40; private const WIND_FACTOR = 0.03; // 3% du vent = dérive Stokes - private const MODEL_VERSION = '1.0.0'; - private const CONCAVITY = 0.8; // paramètre ST_ConcaveHull (0=convex, 1=très concave) + private const MODEL_VERSION = '1.1.0'; + private const CONCAVITY = 0.7; // paramètre ST_ConcaveHull (0=convex, 1=très concave) + private const DIFFUSION_M = 600; // m/heure — diffusion turbulente horizontale (marche aléatoire) public function __construct( private OpenMeteoClient $weather, @@ -155,9 +156,16 @@ class DriftSimulationService $dy = $speed * cos($dirRad) * 3600; // Nord-Sud (m) foreach ($points as &$pt) { - // Conversion m → degrés + // Advection : conversion m → degrés $pt['lat'] += $dy / 111320; $pt['lng'] += $dx / (111320 * cos(deg2rad($pt['lat']))); + + // Diffusion turbulente : marche aléatoire indépendante par point + // Simule la dispersion naturelle du patch sous l'effet des courants de méso-échelle + $perturbLat = (mt_rand(-1000, 1000) / 1000.0) * self::DIFFUSION_M / 111320; + $perturbLng = (mt_rand(-1000, 1000) / 1000.0) * self::DIFFUSION_M / (111320 * cos(deg2rad($pt['lat']))); + $pt['lat'] += $perturbLat; + $pt['lng'] += $perturbLng; } unset($pt); }