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' => $ids],
|
||||||
['ids' => \Doctrine\DBAL\ArrayParameterType::STRING]
|
['ids' => \Doctrine\DBAL\ArrayParameterType::STRING]
|
||||||
);
|
);
|
||||||
// ImpactScore n'a pas de FK vers l'observation — on purge tout et on recalcule
|
// Note : on ne supprime PAS les impact_scores existants.
|
||||||
$this->em->getConnection()->executeStatement('DELETE FROM impact_score');
|
// computeForObservation créera de nouveaux scores ; les anciens restent
|
||||||
|
// pour les spots où cette observation est trop loin (MAX_RELEVANT_M).
|
||||||
$this->em->clear();
|
$this->em->clear();
|
||||||
|
|
||||||
// Recharge les observations détachées
|
// Recharge les observations détachées
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ class DriftSimulationService
|
|||||||
{
|
{
|
||||||
private const HORIZONS = [6, 12, 24, 48];
|
private const HORIZONS = [6, 12, 24, 48];
|
||||||
private const WIND_FACTOR = 0.03; // 3% vitesse vent = dérive de Stokes sargassum
|
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';
|
private const MODEL_VERSION = '2.0.0';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -73,10 +72,7 @@ class DriftSimulationService
|
|||||||
$dxDeg = $totalDxM / (111320.0 * cos(deg2rad($centroid['lat'])));
|
$dxDeg = $totalDxM / (111320.0 * cos(deg2rad($centroid['lat'])));
|
||||||
$dyDeg = $totalDyM / 111320.0;
|
$dyDeg = $totalDyM / 111320.0;
|
||||||
|
|
||||||
// Buffer de diffusion : croît comme √horizon (diffusion Brownienne)
|
$geometry = $this->buildForecastGeometry($id, $dxDeg, $dyDeg);
|
||||||
$bufferM = self::DIFFUSION_M * sqrt($horizon);
|
|
||||||
|
|
||||||
$geometry = $this->buildForecastGeometry($id, $dxDeg, $dyDeg, $bufferM);
|
|
||||||
if ($geometry === null) {
|
if ($geometry === null) {
|
||||||
$this->logger->info("No polygon for H+{$horizon}", ['id' => $id]);
|
$this->logger->info("No polygon for H+{$horizon}", ['id' => $id]);
|
||||||
$prevHorizon = $horizon;
|
$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
|
* Pas de ST_Buffer ici : buffer sur geography produit des géométries volumineuses
|
||||||
* se déplace de façon identique → ST_Distance retourne la vraie distance
|
* et invalides sur de grands MULTIPOLYGON, corrompant les calculs ST_Distance.
|
||||||
* au patch le plus proche, pas au centroïde global.
|
* ST_Translate seul conserve la structure et la taille de chaque patch individuel.
|
||||||
*/
|
*/
|
||||||
private function buildForecastGeometry(
|
private function buildForecastGeometry(
|
||||||
string $observationId,
|
string $observationId,
|
||||||
float $dxDeg,
|
float $dxDeg,
|
||||||
float $dyDeg,
|
float $dyDeg,
|
||||||
float $bufferM,
|
|
||||||
): ?string {
|
): ?string {
|
||||||
$row = $this->connection->fetchAssociative(
|
$row = $this->connection->fetchAssociative(
|
||||||
'SELECT ST_AsText(
|
'SELECT ST_AsText(
|
||||||
ST_Multi(
|
ST_Multi(
|
||||||
ST_Buffer(
|
ST_SimplifyPreserveTopology(
|
||||||
ST_Translate(geometry, :dx, :dy)::geography,
|
ST_Translate(geometry, :dx, :dy),
|
||||||
:buffer
|
0.001
|
||||||
)::geometry
|
)
|
||||||
)
|
)
|
||||||
) AS wkt
|
) AS wkt
|
||||||
FROM sargassum_observation WHERE id = :id',
|
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)
|
return ($row !== false && isset($row['wkt']) && $row['wkt'] !== null)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ use Symfony\Contracts\Cache\ItemInterface;
|
|||||||
class ImpactScoreService
|
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_DENSITY_KM2 = 100; // 100 km² = densité maximale
|
||||||
private const MAX_APPROACH_KMH = 5; // 5 km/h = vitesse d'approche max
|
private const MAX_APPROACH_KMH = 5; // 5 km/h = vitesse d'approche max
|
||||||
private const CACHE_TTL = 10_800; // 3h
|
private const CACHE_TTL = 10_800; // 3h
|
||||||
@@ -68,6 +69,13 @@ class ImpactScoreService
|
|||||||
$spotId = (string) $spot->getId();
|
$spotId = (string) $spot->getId();
|
||||||
|
|
||||||
try {
|
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);
|
$score = $this->computeScore($spot, $obsId, $forecasts);
|
||||||
$this->persistScore($spot, $score);
|
$this->persistScore($spot, $score);
|
||||||
$this->invalidateCache($spotId);
|
$this->invalidateCache($spotId);
|
||||||
|
|||||||
Reference in New Issue
Block a user