From cdb91bbda66b967517e0609bb659931d113c0bc6 Mon Sep 17 00:00:00 2001 From: 95x8x9 Date: Wed, 13 May 2026 20:07:40 +0900 Subject: [PATCH 1/3] [Domain] tune pressure thresholds toward Pathfinder --- src/domain/CompressionSystem.cpp | 51 +++++++++------ src/domain/DemoFixtureService.cpp | 4 +- src/domain/PressureTuning.h | 11 ++++ src/domain/ScenarioRiskMetrics.cpp | 6 +- src/domain/ScenarioRiskMetrics.h | 18 ++++-- src/domain/ScenarioRiskMetricsSystem.cpp | 81 ++++++++++++++---------- src/domain/ScenarioSimulationSystems.cpp | 15 ++--- tests/CompressionSystemTests.cpp | 25 +++++++- tests/ScenarioSimulationSystemsTests.cpp | 14 ++-- 9 files changed, 141 insertions(+), 84 deletions(-) create mode 100644 src/domain/PressureTuning.h diff --git a/src/domain/CompressionSystem.cpp b/src/domain/CompressionSystem.cpp index 6e022b3..5b9b651 100644 --- a/src/domain/CompressionSystem.cpp +++ b/src/domain/CompressionSystem.cpp @@ -3,6 +3,7 @@ #include "domain/AgentComponents.h" #include "domain/FacilityLayout2D.h" #include "domain/Metrics.h" +#include "domain/PressureTuning.h" #include #include @@ -10,8 +11,8 @@ namespace safecrowd::domain { namespace { -constexpr float kForceThreshold = 0.5f; -constexpr float kExposureThreshold = 2.0f; +constexpr double kPi = 3.14159265358979323846; +constexpr double kReferenceDistanceMeters = kPressureReferenceDistanceMeters; double distanceBetween(const Point2D& lhs, const Point2D& rhs) { const double dx = lhs.x - rhs.x; @@ -39,7 +40,7 @@ double distancePointToSegment(const Point2D& point, const Point2D& start, const return distanceBetween(point, projection); } -double barrierCompression(const Barrier2D& barrier, const Point2D& position, double radius) { +double barrierCompression(const Barrier2D& barrier, const Point2D& position, double referenceDistance) { if (!barrier.blocksMovement || barrier.geometry.vertices.size() < 2) { return 0.0; } @@ -49,21 +50,28 @@ double barrierCompression(const Barrier2D& barrier, const Point2D& position, dou for (std::size_t index = 0; index + 1 < vertices.size(); ++index) { const double distance = distancePointToSegment(position, vertices[index], vertices[index + 1]); - if (distance < radius) { - force += radius - distance; + if (distance < referenceDistance) { + force += (referenceDistance - distance) / referenceDistance; } } if (barrier.geometry.closed) { const double distance = distancePointToSegment(position, vertices.back(), vertices.front()); - if (distance < radius) { - force += radius - distance; + if (distance < referenceDistance) { + force += (referenceDistance - distance) / referenceDistance; } } return force; } +double localDensityRatio(std::size_t nearbyCount) { + const auto areaSquareMeters = kPi * kReferenceDistanceMeters * kReferenceDistanceMeters; + const auto densityPeoplePerSquareMeter = + static_cast(nearbyCount) / areaSquareMeters; + return densityPeoplePerSquareMeter / kPressureHighDensityThresholdPeoplePerSquareMeter; +} + } // namespace CompressionSystem::CompressionSystem(double timeStepSeconds) @@ -80,10 +88,10 @@ void CompressionSystem::update(engine::EngineWorld& world, for (const auto entity : agentEntities) { const auto& position = query.get(entity); - const auto& agent = query.get(entity); auto& compression = query.get(entity); - double currentForce = 0.0; + std::size_t nearbyCount = 0; + double proximityScore = 0.0; for (const auto otherEntity : agentEntities) { if (otherEntity == entity) { @@ -91,30 +99,31 @@ void CompressionSystem::update(engine::EngineWorld& world, } const auto& otherPosition = query.get(otherEntity); - const auto& otherAgent = query.get(otherEntity); const double distance = distanceBetween(position.value, otherPosition.value); - const double combinedRadius = static_cast(agent.radius + otherAgent.radius); - - if (distance < combinedRadius) { - currentForce += combinedRadius - distance; + if (distance < kReferenceDistanceMeters) { + proximityScore += (kReferenceDistanceMeters - distance) / kReferenceDistanceMeters; + ++nearbyCount; } } + double currentForce = std::max(proximityScore, localDensityRatio(nearbyCount)); for (const auto barrierEntity : barrierEntities) { - currentForce += barrierCompression( - query.get(barrierEntity), - position.value, - static_cast(agent.radius)); + currentForce = std::max( + currentForce, + proximityScore + barrierCompression( + query.get(barrierEntity), + position.value, + kReferenceDistanceMeters)); } compression.force = static_cast(currentForce); - if (compression.force > kForceThreshold) { + if (compression.force >= kPressureCriticalScoreThreshold) { compression.exposure += timeStepSeconds_; } compression.isCritical = - compression.force > kForceThreshold && - compression.exposure >= kExposureThreshold; + compression.force >= kPressureCriticalScoreThreshold && + compression.exposure >= kPressureCriticalExposureThresholdSeconds; } } diff --git a/src/domain/DemoFixtureService.cpp b/src/domain/DemoFixtureService.cpp index 1746d33..2c86ff2 100644 --- a/src/domain/DemoFixtureService.cpp +++ b/src/domain/DemoFixtureService.cpp @@ -3,6 +3,7 @@ #include #include "domain/DemoLayouts.h" +#include "domain/PressureTuning.h" namespace safecrowd::domain { namespace { @@ -704,7 +705,8 @@ ScenarioResultArtifacts makeBlockedDoorResultArtifacts(std::vector #include "domain/Geometry2D.h" +#include "domain/PressureTuning.h" #include "domain/ScenarioSimulationFrame.h" namespace safecrowd::domain { inline constexpr double kScenarioStalledSpeedThreshold = 0.12; inline constexpr double kScenarioStalledSecondsThreshold = 0.75; -inline constexpr double kScenarioHotspotCellSize = 1.5; -inline constexpr std::size_t kScenarioHotspotAgentThreshold = 5; -inline constexpr std::size_t kScenarioPressureHotspotAgentThreshold = 5; -inline constexpr double kScenarioPressureScoreThreshold = 1.0; +inline constexpr double kScenarioHotspotCellSize = kPressureHotspotCellSizeMeters; +inline constexpr double kScenarioHotspotDensityThresholdPeoplePerSquareMeter = + kPressureHighDensityThresholdPeoplePerSquareMeter; +inline constexpr double kScenarioPressureHotspotDensityThresholdPeoplePerSquareMeter = + kPressureHighDensityThresholdPeoplePerSquareMeter; +inline constexpr double kScenarioPressureScoreThreshold = kPressureCriticalScoreThreshold; inline constexpr double kScenarioPressureFeedbackForceThreshold = 0.18; inline constexpr double kScenarioPressureFeedbackExposureRecoveryPerSecond = 1.0; -inline constexpr double kScenarioPressureFeedbackNeighborProbeRadius = 0.9; +inline constexpr double kScenarioPressureFeedbackNeighborProbeRadius = kPressureReferenceDistanceMeters; inline constexpr std::size_t kScenarioPressureFeedbackDenseNeighborThreshold = 2; inline constexpr std::uint64_t kScenarioPressureFeedbackCrowdedUpdateDivisor = 2; inline constexpr std::uint64_t kScenarioPressureFeedbackQuietUpdateDivisor = 3; @@ -27,8 +30,9 @@ inline constexpr double kScenarioPressureFeedbackMaxExposedSlowdown = 0.10; inline constexpr double kScenarioPressureFeedbackMaxCriticalSlowdown = 0.25; inline constexpr double kScenarioPressureFeedbackMaxAvoidanceScale = 1.35; inline constexpr double kScenarioPressureFeedbackMaxBarrierScale = 1.25; -inline constexpr double kScenarioCriticalPressureForceThreshold = 0.5; -inline constexpr double kScenarioCriticalPressureExposureThresholdSeconds = 2.0; +inline constexpr double kScenarioCriticalPressureForceThreshold = kPressureCriticalScoreThreshold; +inline constexpr double kScenarioCriticalPressureExposureThresholdSeconds = + kPressureCriticalExposureThresholdSeconds; inline constexpr double kScenarioCriticalPressureEventDurationThresholdSeconds = 1.0; inline constexpr std::size_t kScenarioCriticalPressureEventAgentThreshold = 2; inline constexpr double kScenarioBottleneckRadius = 1.25; diff --git a/src/domain/ScenarioRiskMetricsSystem.cpp b/src/domain/ScenarioRiskMetricsSystem.cpp index cd2b735..679ed8b 100644 --- a/src/domain/ScenarioRiskMetricsSystem.cpp +++ b/src/domain/ScenarioRiskMetricsSystem.cpp @@ -20,7 +20,6 @@ constexpr std::size_t kMaxReportedPressureHotspots = 5; constexpr std::size_t kMaxReportedPressureAgents = 5; constexpr std::size_t kMaxReportedCriticalPressureEvents = 5; constexpr std::size_t kMaxReportedBottlenecks = 5; -constexpr double kScenarioPressureScoreThreshold = 1.0; struct RiskCellAccumulator { Point2D positionSum{}; @@ -235,7 +234,7 @@ bool barrierMatchesFloor(const Barrier2D& barrier, const std::string& floorId) { return barrier.floorId.empty() || floorId.empty() || barrier.floorId == floorId; } -double barrierCompression(const Barrier2D& barrier, const Point2D& position, double radius) { +double barrierCompression(const Barrier2D& barrier, const Point2D& position, double referenceDistance) { if (!barrier.blocksMovement || barrier.geometry.vertices.size() < 2) { return 0.0; } @@ -244,19 +243,27 @@ double barrierCompression(const Barrier2D& barrier, const Point2D& position, dou const auto& vertices = barrier.geometry.vertices; for (std::size_t index = 0; index + 1 < vertices.size(); ++index) { const auto distance = distancePointToSegment(position, vertices[index], vertices[index + 1]); - if (distance < radius) { - force += radius - distance; + if (distance < referenceDistance) { + force += (referenceDistance - distance) / referenceDistance; } } if (barrier.geometry.closed) { const auto distance = distancePointToSegment(position, vertices.back(), vertices.front()); - if (distance < radius) { - force += radius - distance; + if (distance < referenceDistance) { + force += (referenceDistance - distance) / referenceDistance; } } return force; } +double localDensityRatio(std::size_t nearbyCount) { + constexpr double kPi = 3.14159265358979323846; + const auto areaSquareMeters = kPi * kPressureReferenceDistanceMeters * kPressureReferenceDistanceMeters; + const auto densityPeoplePerSquareMeter = + static_cast(nearbyCount) / areaSquareMeters; + return densityPeoplePerSquareMeter / kPressureHighDensityThresholdPeoplePerSquareMeter; +} + double pressureFeedbackLevel(double compressionForce, double exposureSeconds, bool critical) { if (critical) { return 1.0; @@ -774,33 +781,40 @@ class ScenarioRiskMetricsSystem final : public engine::EngineSystem { tracking.previousElapsedSeconds = elapsedSeconds; tracking.hasPreviousElapsedSeconds = true; - std::vector forces(activeAgents.size(), 0.0); + std::vector proximityForces(activeAgents.size(), 0.0); + std::vector nearbyCounts(activeAgents.size(), 0); for (std::size_t lhsIndex = 0; lhsIndex < activeAgents.size(); ++lhsIndex) { for (std::size_t rhsIndex = lhsIndex + 1; rhsIndex < activeAgents.size(); ++rhsIndex) { if (activeAgents[lhsIndex].floorId != activeAgents[rhsIndex].floorId) { continue; } const auto distance = distanceBetween(activeAgents[lhsIndex].position, activeAgents[rhsIndex].position); - const auto combinedRadius = activeAgents[lhsIndex].radius + activeAgents[rhsIndex].radius; - if (distance >= combinedRadius) { + if (distance >= kPressureReferenceDistanceMeters) { continue; } - const auto overlap = combinedRadius - distance; - forces[lhsIndex] += overlap; - forces[rhsIndex] += overlap; + const auto proximityScore = + (kPressureReferenceDistanceMeters - distance) / kPressureReferenceDistanceMeters; + proximityForces[lhsIndex] += proximityScore; + proximityForces[rhsIndex] += proximityScore; + ++nearbyCounts[lhsIndex]; + ++nearbyCounts[rhsIndex]; } } + std::vector forces(activeAgents.size(), 0.0); for (std::size_t index = 0; index < activeAgents.size(); ++index) { + forces[index] = std::max(proximityForces[index], localDensityRatio(nearbyCounts[index])); for (const auto& barrier : layout.barriers) { if (!barrierMatchesFloor(barrier, activeAgents[index].floorId)) { continue; } - forces[index] += barrierCompression( - barrier, - activeAgents[index].position, - activeAgents[index].radius); + forces[index] = std::max( + forces[index], + proximityForces[index] + barrierCompression( + barrier, + activeAgents[index].position, + kPressureReferenceDistanceMeters)); } } @@ -812,14 +826,14 @@ class ScenarioRiskMetricsSystem final : public engine::EngineSystem { activeAgentIds.insert(context.agentId); auto& state = tracking.agentStates[context.agentId]; state.currentForce = forces[index]; - if (state.currentForce > kScenarioCriticalPressureForceThreshold) { + if (state.currentForce >= kScenarioCriticalPressureForceThreshold) { state.exposureSeconds += deltaSeconds; } const bool critical = - state.currentForce > kScenarioCriticalPressureForceThreshold + state.currentForce >= kScenarioCriticalPressureForceThreshold && state.exposureSeconds >= kScenarioCriticalPressureExposureThresholdSeconds; - if (state.exposureSeconds > 0.0 || state.currentForce > kScenarioCriticalPressureForceThreshold) { + if (state.exposureSeconds > 0.0 || state.currentForce >= kScenarioCriticalPressureForceThreshold) { ++snapshot.pressureExposedAgentCount; snapshot.pressureAgents.push_back({ .agentId = context.agentId, @@ -863,8 +877,12 @@ class ScenarioRiskMetricsSystem final : public engine::EngineSystem { ScenarioRiskSnapshot& snapshot, const std::unordered_map& cells) const { snapshot.hotspots.reserve(cells.size()); + const auto hotspotCellArea = kScenarioHotspotCellSize * kScenarioHotspotCellSize; for (const auto& [_, cell] : cells) { - if (cell.agentCount < kScenarioHotspotAgentThreshold) { + const auto densityPeoplePerSquareMeter = hotspotCellArea <= 0.0 + ? 0.0 + : static_cast(cell.agentCount) / hotspotCellArea; + if (densityPeoplePerSquareMeter < kScenarioHotspotDensityThresholdPeoplePerSquareMeter) { continue; } const auto count = static_cast(cell.agentCount); @@ -893,7 +911,10 @@ class ScenarioRiskMetricsSystem final : public engine::EngineSystem { const auto cellArea = kScenarioHotspotCellSize * kScenarioHotspotCellSize; for (const auto& [_, cell] : cells) { - if (cell.agentCount < kScenarioPressureHotspotAgentThreshold) { + const auto densityPeoplePerSquareMeter = cellArea <= 1e-9 + ? 0.0 + : static_cast(cell.agentCount) / cellArea; + if (densityPeoplePerSquareMeter < kScenarioPressureHotspotDensityThresholdPeoplePerSquareMeter) { continue; } @@ -902,22 +923,16 @@ class ScenarioRiskMetricsSystem final : public engine::EngineSystem { for (std::size_t lhsIndex = 0; lhsIndex < cell.entities.size(); ++lhsIndex) { const auto lhsEntity = cell.entities[lhsIndex]; const auto& lhsPosition = query.get(lhsEntity); - const auto& lhsAgent = query.get(lhsEntity); for (std::size_t rhsIndex = lhsIndex + 1; rhsIndex < cell.entities.size(); ++rhsIndex) { const auto rhsEntity = cell.entities[rhsIndex]; const auto& rhsPosition = query.get(rhsEntity); - const auto& rhsAgent = query.get(rhsEntity); - const auto comfortDistance = - static_cast(lhsAgent.radius + rhsAgent.radius) + kPersonalSpaceBuffer; - if (comfortDistance <= 1e-9) { - continue; - } const auto distance = distanceBetween(lhsPosition.value, rhsPosition.value); - if (distance >= comfortDistance) { + if (distance >= kPressureReferenceDistanceMeters) { continue; } - pressureScore += (comfortDistance - distance) / comfortDistance; + pressureScore += + (kPressureReferenceDistanceMeters - distance) / kPressureReferenceDistanceMeters; ++intrudingPairCount; } } @@ -934,7 +949,7 @@ class ScenarioRiskMetricsSystem final : public engine::EngineSystem { .floorId = cell.floorId, .agentCount = cell.agentCount, .intrudingPairCount = intrudingPairCount, - .densityPeoplePerSquareMeter = cellArea <= 1e-9 ? 0.0 : count / cellArea, + .densityPeoplePerSquareMeter = densityPeoplePerSquareMeter, .pressureScore = pressureScore, }); } @@ -975,9 +990,9 @@ class ScenarioRiskMetricsSystem final : public engine::EngineSystem { const auto& state = stateIt->second; const bool exposed = state.exposureSeconds > 0.0 - || state.currentForce > kScenarioCriticalPressureForceThreshold; + || state.currentForce >= kScenarioCriticalPressureForceThreshold; const bool critical = - state.currentForce > kScenarioCriticalPressureForceThreshold + state.currentForce >= kScenarioCriticalPressureForceThreshold && state.exposureSeconds >= kScenarioCriticalPressureExposureThresholdSeconds; if (exposed) { ++exposedAgentCount; diff --git a/src/domain/ScenarioSimulationSystems.cpp b/src/domain/ScenarioSimulationSystems.cpp index ab6812c..72d5adb 100644 --- a/src/domain/ScenarioSimulationSystems.cpp +++ b/src/domain/ScenarioSimulationSystems.cpp @@ -24,7 +24,8 @@ constexpr std::size_t kMaxResultPressureCells = 5; constexpr std::size_t kMaxResultPressureHotspots = 5; constexpr std::size_t kMaxResultPressureAgents = 5; constexpr std::size_t kMaxResultCriticalPressureEvents = 5; -constexpr double kHighDensityThresholdPeoplePerSquareMeter = 4.0; +constexpr double kHighDensityThresholdPeoplePerSquareMeter = + kPressureHighDensityThresholdPeoplePerSquareMeter; struct SpatialCell { int x{0}; @@ -175,22 +176,16 @@ PressureCellMetric pressureMetricFromCell( for (std::size_t lhsIndex = 0; lhsIndex < cell.entities.size(); ++lhsIndex) { const auto lhsEntity = cell.entities[lhsIndex]; const auto& lhsPosition = query.get(lhsEntity); - const auto& lhsAgent = query.get(lhsEntity); for (std::size_t rhsIndex = lhsIndex + 1; rhsIndex < cell.entities.size(); ++rhsIndex) { const auto rhsEntity = cell.entities[rhsIndex]; const auto& rhsPosition = query.get(rhsEntity); - const auto& rhsAgent = query.get(rhsEntity); - const auto comfortDistance = - static_cast(lhsAgent.radius + rhsAgent.radius) + simulation_internal::kPersonalSpaceBuffer; - if (comfortDistance <= 1e-9) { - continue; - } const auto distance = distanceBetween(lhsPosition.value, rhsPosition.value); - if (distance >= comfortDistance) { + if (distance >= kPressureReferenceDistanceMeters) { continue; } - metric.pressureScore += (comfortDistance - distance) / comfortDistance; + metric.pressureScore += + (kPressureReferenceDistanceMeters - distance) / kPressureReferenceDistanceMeters; ++metric.intrudingPairCount; } } diff --git a/tests/CompressionSystemTests.cpp b/tests/CompressionSystemTests.cpp index 46663e5..be2887b 100644 --- a/tests/CompressionSystemTests.cpp +++ b/tests/CompressionSystemTests.cpp @@ -60,7 +60,7 @@ SC_TEST(CompressionSystem_UpdatesAgentOverlapWithoutBarrierEntitiesAndPreservesE system.update(world, {}); const auto& clusteredMetrics = world.query().get(first); - SC_EXPECT_TRUE(clusteredMetrics.force > 0.5f); + SC_EXPECT_TRUE(clusteredMetrics.force >= 1.0f); SC_EXPECT_NEAR(clusteredMetrics.exposure, 0.5, 1e-6); SC_EXPECT_TRUE(!clusteredMetrics.isCritical); @@ -74,6 +74,27 @@ SC_TEST(CompressionSystem_UpdatesAgentOverlapWithoutBarrierEntitiesAndPreservesE SC_EXPECT_TRUE(!separatedMetrics.isCritical); } +SC_TEST(CompressionSystem_TracksPressureFromNearbyAgentsWithinOneMeterWithoutOverlap) { + EcsCore core; + safecrowd::engine::ResourceStore resources; + CommandBuffer buffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, resources, buffer); + + const Entity first = addAgent(core, 0.0, 0.0); + addAgent(core, 0.6, 0.0); + addAgent(core, -0.6, 0.0); + addAgent(core, 0.0, 0.6); + addAgent(core, 0.0, -0.6); + + CompressionSystem system(0.5); + system.update(world, {}); + + const auto& metrics = world.query().get(first); + SC_EXPECT_TRUE(metrics.force >= 1.0f); + SC_EXPECT_NEAR(metrics.exposure, 0.5, 1e-6); + SC_EXPECT_TRUE(!metrics.isCritical); +} + SC_TEST(CompressionSystem_CombinesExposureWithCurrentForceForCriticalState) { EcsCore core; safecrowd::engine::ResourceStore resources; @@ -90,7 +111,7 @@ SC_TEST(CompressionSystem_CombinesExposureWithCurrentForceForCriticalState) { system.update(world, {}); const auto& highRiskMetrics = world.query().get(first); - SC_EXPECT_TRUE(highRiskMetrics.force > 0.5f); + SC_EXPECT_TRUE(highRiskMetrics.force >= 1.0f); SC_EXPECT_NEAR(highRiskMetrics.exposure, 2.0, 1e-6); SC_EXPECT_TRUE(highRiskMetrics.isCritical); diff --git a/tests/ScenarioSimulationSystemsTests.cpp b/tests/ScenarioSimulationSystemsTests.cpp index dde40d1..88cbe33 100644 --- a/tests/ScenarioSimulationSystemsTests.cpp +++ b/tests/ScenarioSimulationSystemsTests.cpp @@ -2460,7 +2460,7 @@ SC_TEST(ScenarioSimulationMotionSystem_CombinesHazardExposureWithDoorClosureRero SC_TEST(ScenarioRiskMetricsSystem_PublishesStalledHotspotAndBottleneckMetrics) { std::vector seeds; - for (int index = 0; index < 5; ++index) { + for (int index = 0; index < 8; ++index) { seeds.push_back({ .position = {.value = {.x = 0.75 + (static_cast(index) * 0.03), .y = 0.0}}, .agent = {.radius = 0.25f, .maxSpeed = 1.0f}, @@ -2496,14 +2496,14 @@ SC_TEST(ScenarioRiskMetricsSystem_PublishesStalledHotspotAndBottleneckMetrics) { const auto& snapshot = runtime.world().resources().get().snapshot; - SC_EXPECT_EQ(snapshot.stalledAgentCount, std::size_t{5}); + SC_EXPECT_EQ(snapshot.stalledAgentCount, std::size_t{8}); SC_EXPECT_TRUE(!snapshot.hotspots.empty()); SC_EXPECT_NEAR(snapshot.hotspots.front().cellMin.x, 0.0, 1e-9); SC_EXPECT_NEAR(snapshot.hotspots.front().cellMin.y, 0.0, 1e-9); SC_EXPECT_NEAR(snapshot.hotspots.front().cellMax.x, 1.5, 1e-9); SC_EXPECT_NEAR(snapshot.hotspots.front().cellMax.y, 1.5, 1e-9); SC_EXPECT_TRUE(!snapshot.pressureHotspots.empty()); - SC_EXPECT_EQ(snapshot.pressureHotspots.front().agentCount, std::size_t{5}); + SC_EXPECT_EQ(snapshot.pressureHotspots.front().agentCount, std::size_t{8}); SC_EXPECT_TRUE(snapshot.pressureHotspots.front().intrudingPairCount > 0); SC_EXPECT_TRUE(snapshot.pressureHotspots.front().pressureScore >= 1.0); SC_EXPECT_TRUE(!snapshot.bottlenecks.empty()); @@ -2585,7 +2585,7 @@ SC_TEST(ScenarioRiskMetricsSystem_AccumulatesCompressionExposureAndCriticalPress SC_EXPECT_TRUE(snapshot.criticalPressureAgentCount > 0); SC_EXPECT_TRUE(!snapshot.pressureAgents.empty()); SC_EXPECT_TRUE(snapshot.pressureAgents.front().critical); - SC_EXPECT_TRUE(snapshot.pressureAgents.front().compressionForce > 0.5); + SC_EXPECT_TRUE(snapshot.pressureAgents.front().compressionForce >= 1.0); SC_EXPECT_TRUE(snapshot.pressureAgents.front().exposureSeconds >= 2.0); } @@ -2670,7 +2670,7 @@ SC_TEST(ScenarioRiskMetricsSystem_FiltersBottlenecksByConnectionFloor) { SC_TEST(ScenarioRiskMetricsSystem_UsesDisplayFloorForVirtualPhysicsBuckets) { std::vector seeds; - for (int index = 0; index < 5; ++index) { + for (int index = 0; index < 8; ++index) { seeds.push_back({ .position = {.value = {.x = 0.75 + (static_cast(index) * 0.03), .y = 0.0}}, .agent = {.radius = 0.25f, .maxSpeed = 1.0f}, @@ -2719,7 +2719,7 @@ SC_TEST(ScenarioRiskMetricsSystem_UsesDisplayFloorForVirtualPhysicsBuckets) { SC_TEST(ScenarioRiskMetricsSystem_PreservesPeakMetricsAfterAllAgentsEvacuate) { std::vector seeds; - for (int index = 0; index < 5; ++index) { + for (int index = 0; index < 8; ++index) { seeds.push_back({ .position = {.value = {.x = 0.75 + (static_cast(index) * 0.03), .y = 0.0}}, .agent = {.radius = 0.25f, .maxSpeed = 1.0f}, @@ -2767,7 +2767,7 @@ SC_TEST(ScenarioRiskMetricsSystem_PreservesPeakMetricsAfterAllAgentsEvacuate) { SC_EXPECT_TRUE(!metrics.peakSnapshot.hotspots.empty()); SC_EXPECT_TRUE(!metrics.peakSnapshot.pressureHotspots.empty()); SC_EXPECT_TRUE(!metrics.peakSnapshot.bottlenecks.empty()); - SC_EXPECT_EQ(metrics.peakSnapshot.stalledAgentCount, std::size_t{5}); + SC_EXPECT_EQ(metrics.peakSnapshot.stalledAgentCount, std::size_t{8}); SC_EXPECT_EQ(metrics.peakSnapshot.completionRisk, safecrowd::domain::ScenarioRiskLevel::High); } From 345f66044c836bcb7bf19b59a114daed5eb43bb5 Mon Sep 17 00:00:00 2001 From: 95x8x9 Date: Wed, 13 May 2026 20:18:30 +0900 Subject: [PATCH 2/3] [Domain] align loose-cluster hotspot expectations --- tests/ScenarioSimulationSystemsTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ScenarioSimulationSystemsTests.cpp b/tests/ScenarioSimulationSystemsTests.cpp index 88cbe33..dafe364 100644 --- a/tests/ScenarioSimulationSystemsTests.cpp +++ b/tests/ScenarioSimulationSystemsTests.cpp @@ -2555,7 +2555,7 @@ SC_TEST(ScenarioRiskMetricsSystem_DoesNotPublishPressureHotspotsForLooseClusterI const auto& snapshot = runtime.world().resources().get().snapshot; - SC_EXPECT_TRUE(!snapshot.hotspots.empty()); + SC_EXPECT_TRUE(snapshot.hotspots.empty()); SC_EXPECT_TRUE(snapshot.pressureHotspots.empty()); } From 476cb43304665f7370f10d2ebe1a11d60a3f517e Mon Sep 17 00:00:00 2001 From: learncold Date: Thu, 14 May 2026 13:51:31 +0900 Subject: [PATCH 3/3] Keep pressure feedback off vertical transitions --- src/domain/ScenarioSimulationMotionSystem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/ScenarioSimulationMotionSystem.cpp b/src/domain/ScenarioSimulationMotionSystem.cpp index 2d58b52..60dd886 100644 --- a/src/domain/ScenarioSimulationMotionSystem.cpp +++ b/src/domain/ScenarioSimulationMotionSystem.cpp @@ -212,7 +212,7 @@ class ScenarioSimulationMotionSystem final : public engine::EngineSystem { double pressureSpeedFactor = 1.0; double pressureAvoidanceScale = 1.0; double pressureBarrierScale = 1.0; - if (pressureFeedback != nullptr) { + if (!verticalTransition && pressureFeedback != nullptr) { const auto feedbackIt = pressureFeedback->agentsById.find(entity.index); if (feedbackIt != pressureFeedback->agentsById.end()) { pressureSpeedFactor = feedbackIt->second.speedFactor;