fix: 3 bugs introduits lors du refactoring DriftSimulation/--force
1. DriftSimulationService : suppression ST_Buffer(::geography) — produisait des géométries invalides/dégénérées sur grand MULTIPOLYGON → ST_Distance retournait 652 029km. Remplacé par ST_Translate + ST_SimplifyPreserveTopology uniquement. 2. ImpactScoreService : filtre MAX_RELEVANT_M (300km) — une observation distante ne remplace plus le score d'un spot proche. Les scores existants sont préservés si l'observation est hors zone pertinente. 3. ComputeForecastsCommand --force : suppression du DELETE FROM impact_score. On ne purge plus les scores existants ; computeForObservation ajoute les nouveaux scores, les anciens restent valides pour les spots éloignés. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -70,8 +70,9 @@ class ComputeForecastsCommand extends Command
|
||||
['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');
|
||||
// Note : on ne supprime PAS les impact_scores existants.
|
||||
// computeForObservation créera de nouveaux scores ; les anciens restent
|
||||
// pour les spots où cette observation est trop loin (MAX_RELEVANT_M).
|
||||
$this->em->clear();
|
||||
|
||||
// Recharge les observations détachées
|
||||
|
||||
@@ -27,7 +27,6 @@ class DriftSimulationService
|
||||
{
|
||||
private const HORIZONS = [6, 12, 24, 48];
|
||||
private const WIND_FACTOR = 0.03; // 3% vitesse vent = dérive de Stokes sargassum
|
||||
private const DIFFUSION_M = 1500; // m — coefficient de diffusion (rayon du buffer à 1h)
|
||||
private const MODEL_VERSION = '2.0.0';
|
||||
|
||||
public function __construct(
|
||||
@@ -73,10 +72,7 @@ class DriftSimulationService
|
||||
$dxDeg = $totalDxM / (111320.0 * cos(deg2rad($centroid['lat'])));
|
||||
$dyDeg = $totalDyM / 111320.0;
|
||||
|
||||
// Buffer de diffusion : croît comme √horizon (diffusion Brownienne)
|
||||
$bufferM = self::DIFFUSION_M * sqrt($horizon);
|
||||
|
||||
$geometry = $this->buildForecastGeometry($id, $dxDeg, $dyDeg, $bufferM);
|
||||
$geometry = $this->buildForecastGeometry($id, $dxDeg, $dyDeg);
|
||||
if ($geometry === null) {
|
||||
$this->logger->info("No polygon for H+{$horizon}", ['id' => $id]);
|
||||
$prevHorizon = $horizon;
|
||||
@@ -122,29 +118,28 @@ class DriftSimulationService
|
||||
}
|
||||
|
||||
/**
|
||||
* Traduit la géométrie source de $dxDeg/$dyDeg degrés, puis dilate de $bufferM mètres.
|
||||
* Traduit la géométrie source de $dxDeg/$dyDeg degrés via ST_Translate.
|
||||
*
|
||||
* ST_Translate conserve la structure MULTIPOLYGON : chaque patch individuel
|
||||
* se déplace de façon identique → ST_Distance retourne la vraie distance
|
||||
* au patch le plus proche, pas au centroïde global.
|
||||
* Pas de ST_Buffer ici : buffer sur geography produit des géométries volumineuses
|
||||
* et invalides sur de grands MULTIPOLYGON, corrompant les calculs ST_Distance.
|
||||
* ST_Translate seul conserve la structure et la taille de chaque patch individuel.
|
||||
*/
|
||||
private function buildForecastGeometry(
|
||||
string $observationId,
|
||||
float $dxDeg,
|
||||
float $dyDeg,
|
||||
float $bufferM,
|
||||
): ?string {
|
||||
$row = $this->connection->fetchAssociative(
|
||||
'SELECT ST_AsText(
|
||||
ST_Multi(
|
||||
ST_Buffer(
|
||||
ST_Translate(geometry, :dx, :dy)::geography,
|
||||
:buffer
|
||||
)::geometry
|
||||
ST_SimplifyPreserveTopology(
|
||||
ST_Translate(geometry, :dx, :dy),
|
||||
0.001
|
||||
)
|
||||
)
|
||||
) AS wkt
|
||||
FROM sargassum_observation WHERE id = :id',
|
||||
['id' => $observationId, 'dx' => $dxDeg, 'dy' => $dyDeg, 'buffer' => $bufferM]
|
||||
['id' => $observationId, 'dx' => $dxDeg, 'dy' => $dyDeg]
|
||||
);
|
||||
|
||||
return ($row !== false && isset($row['wkt']) && $row['wkt'] !== null)
|
||||
|
||||
@@ -27,7 +27,8 @@ use Symfony\Contracts\Cache\ItemInterface;
|
||||
*/
|
||||
class ImpactScoreService
|
||||
{
|
||||
private const MAX_DISTANCE_M = 50_000; // 50 km : au-delà score distance = 0
|
||||
private const MAX_DISTANCE_M = 50_000; // 50 km : au-delà score distance = 0
|
||||
private const MAX_RELEVANT_M = 300_000; // 300 km : au-delà, on ne touche pas au score existant
|
||||
private const MAX_DENSITY_KM2 = 100; // 100 km² = densité maximale
|
||||
private const MAX_APPROACH_KMH = 5; // 5 km/h = vitesse d'approche max
|
||||
private const CACHE_TTL = 10_800; // 3h
|
||||
@@ -68,6 +69,13 @@ class ImpactScoreService
|
||||
$spotId = (string) $spot->getId();
|
||||
|
||||
try {
|
||||
// Ne pas écraser le score d'un spot si cette observation est trop loin.
|
||||
// Cela évite qu'une ingestion distante remplace un bon score existant.
|
||||
$distanceCheck = $this->getDistanceToNearestSargassum($spotId, $obsId);
|
||||
if ($distanceCheck === null || $distanceCheck > self::MAX_RELEVANT_M) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$score = $this->computeScore($spot, $obsId, $forecasts);
|
||||
$this->persistScore($spot, $score);
|
||||
$this->invalidateCache($spotId);
|
||||
|
||||
Reference in New Issue
Block a user