From 8bab15d6fcb7dad09aea549eb81314038c05fe60 Mon Sep 17 00:00:00 2001 From: Silversupplier Date: Wed, 13 May 2026 02:57:09 +0900 Subject: [PATCH] [Analysis] Add hazard closure regression coverage --- ...4\355\227\230 \353\252\250\353\215\270.md" | 1 + ...4\355\227\230 \354\240\225\354\235\230.md" | 8 ++ tests/ScenarioSimulationSystemsTests.cpp | 109 ++++++++++++++++++ 3 files changed, 118 insertions(+) diff --git "a/docs/product/\352\263\240\352\270\211 \354\234\204\355\227\230 \353\252\250\353\215\270.md" "b/docs/product/\352\263\240\352\270\211 \354\234\204\355\227\230 \353\252\250\353\215\270.md" index c37ab28..e7c7ca0 100644 --- "a/docs/product/\352\263\240\352\270\211 \354\234\204\355\227\230 \353\252\250\353\215\270.md" +++ "b/docs/product/\352\263\240\352\270\211 \354\234\204\355\227\230 \353\252\250\353\215\270.md" @@ -37,6 +37,7 @@ - 관련 문서: [Calculating Occupant Visibility through Smoke](https://www.thunderheadeng.com/docs/2026-1/results/viewing-pathfinder-output/visibility/), [Calculating Fractional Effective Dose (FED)](https://www.thunderheadeng.com/docs/2026-1/results/viewing-pathfinder-output/fed/), [Coupling with PyroSim (FDS) Simulations](https://www.thunderheadeng.com/docs/2026-1/pathfinder/advanced/coupling-fds/) - 여기서 바로 가져올 수 있는 것은 `시야 저하`, `친숙도`, `유도 신호`, `연기/FED 후처리`의 단계적 결합이다. - 반면 연기 중 독자 길찾기 알고리즘이나 세부 인지 심리 모델까지 Pathfinder가 정식 기능으로 명세하는 것은 아니다. +- 현재 v2 구현은 화재/연기 authoring, 단순 반응, 노출 요약, replay 표시까지의 경량 범위다. FDS coupling, FED 계산, 화재 확산, 연기 농도/가시거리장 시뮬레이션은 아직 포함하지 않는다. ### 2.4. 검증 시나리오로 보는 고급 거동 - Pathfinder는 merging, bidirectional flow, stair passing, elevator loading 같은 거동을 제품 기능 자체보다 `검증 시나리오`로 많이 다룬다. diff --git "a/docs/product/\354\234\204\355\227\230 \354\240\225\354\235\230.md" "b/docs/product/\354\234\204\355\227\230 \354\240\225\354\235\230.md" index 588b3ea..e3b4b77 100644 --- "a/docs/product/\354\234\204\355\227\230 \354\240\225\354\235\230.md" +++ "b/docs/product/\354\234\204\355\227\230 \354\240\225\354\235\230.md" @@ -64,6 +64,14 @@ Pathfinder 2026.1 공식 문서를 다시 확인한 결과, 현재 제품 기능 - 즉, 이 문서의 네 위험 축은 제품 개념 모델로 유지하되, 각 축의 구현 깊이는 같은 단계로 올라가지 않는다. +### v2 구현 한계 +- v2의 화재/연기 기능은 시나리오 작성, 단순 반응, 경로 회피, 노출 요약, 결과 표시까지를 다루는 경량 모델이다. +- FDS/PyroSim 출력과의 직접 coupling은 포함하지 않는다. +- FED, 독성 가스 용량, 생리학적 피해 판정은 계산하지 않는다. +- 화재 확산, 열 방출률 변화, 구역 간 전파는 시뮬레이션하지 않는다. +- 연기 농도장이나 상세 가시거리장을 계산하지 않는다. 현재 연기 영향은 위치 기반 반경과 단순 속도/시야 저하 proxy로 제한한다. +- Pathfinder 문서 기준의 visibility/FED/FDS 후처리 파이프라인은 `중기 확장` 또는 별도 issue 범위로 남긴다. + ## 1. SafeCrowd 기본 위험 체계 초기 제품 문서에서는 위험을 아래 네 축으로 정의한다. diff --git a/tests/ScenarioSimulationSystemsTests.cpp b/tests/ScenarioSimulationSystemsTests.cpp index 1f5bf29..dde40d1 100644 --- a/tests/ScenarioSimulationSystemsTests.cpp +++ b/tests/ScenarioSimulationSystemsTests.cpp @@ -516,6 +516,30 @@ void addClosureMotionSystems( .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); } +void addHazardClosureMotionSystems( + safecrowd::engine::EngineRuntime& runtime, + const safecrowd::domain::FacilityLayout2D& layout, + std::vector hazards, + std::vector blocks) { + runtime.addSystem( + safecrowd::domain::makeScenarioControlSystem(layout, std::move(blocks)), + {.phase = safecrowd::engine::UpdatePhase::PreSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); + runtime.addSystem( + safecrowd::domain::makeScenarioEnvironmentHazardSystem(layout, std::move(hazards)), + {.phase = safecrowd::engine::UpdatePhase::PostSimulation, + .order = -20, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); + runtime.addSystem( + safecrowd::domain::makeScenarioSimulationMotionSystem(layout), + {.phase = safecrowd::engine::UpdatePhase::PostSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); + runtime.addSystem( + std::make_unique(), + {.phase = safecrowd::engine::UpdatePhase::RenderSync, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); +} + void stepScenarioRuntime(safecrowd::engine::EngineRuntime& runtime, double deltaSeconds) { runtime.world().resources().set(safecrowd::domain::ScenarioSimulationStepResource{.deltaSeconds = deltaSeconds}); runtime.stepFrame(0.0); @@ -2349,6 +2373,91 @@ SC_TEST(ScenarioSimulationMotionSystem_RetriesNoExitAfterFiniteDoorClosureReopen SC_EXPECT_TRUE(routeRecovered); } +SC_TEST(ScenarioSimulationMotionSystem_CombinesHazardExposureWithDoorClosureReroute) { + auto layout = wideTwoExitHazardRouteLayout(); + safecrowd::domain::ConnectionBlockDraft block; + block.id = "block-near-exit"; + block.connectionId = "room-near-exit"; + + auto seed = doorRouteSeed( + {.x = 8.4, .y = 1.0}, + "near-exit", + "room-near-exit", + {{.x = 10.0, .y = 0.7}, {.x = 10.0, .y = 1.3}}, + 1.0, + 0.2); + seed.agent.reactionDelaySeconds = 10.0; + seed.agent.hazardSensitivity = 1.0; + seed.agent.smokeSensitivity = 1.0; + + auto fire = hazardDraft( + "combined-fire", + safecrowd::domain::EnvironmentHazardKind::Fire, + safecrowd::domain::ScenarioElementSeverity::Low, + {.x = 8.4, .y = 1.0}, + "room"); + auto smoke = hazardDraft( + "combined-smoke", + safecrowd::domain::EnvironmentHazardKind::Smoke, + safecrowd::domain::ScenarioElementSeverity::Low, + {.x = 8.6, .y = 1.0}, + "room"); + + safecrowd::engine::EngineRuntime runtime({ + .fixedDeltaTime = 0.1, + .maxCatchUpSteps = 1, + .baseSeed = 94, + }); + runtime.addSystem(std::make_unique( + std::vector{seed}, + 5.0)); + addHazardClosureMotionSystems(runtime, layout, {fire, smoke}, {block}); + + runtime.play(); + stepScenarioRuntime(runtime, 0.1); + + auto& query = runtime.world().query(); + const auto entities = query.view< + safecrowd::domain::Position, + safecrowd::domain::Velocity, + safecrowd::domain::EvacuationRoute>(); + SC_EXPECT_EQ(entities.size(), std::size_t{1}); + const auto entity = entities.front(); + + const auto& firstState = + runtime.world().resources().get().agentsById.at(entity.index); + SC_EXPECT_TRUE(firstState.hazardDetected); + SC_EXPECT_TRUE(!firstState.hazardAware); + SC_EXPECT_TRUE(firstState.closureDetected); + SC_EXPECT_TRUE(!firstState.closureAware); + SC_EXPECT_EQ(firstState.blockedConnectionId, std::string{"room-near-exit"}); + + const auto& activeHazards = + runtime.world().resources().get(); + SC_EXPECT_EQ(activeHazards.hazards.size(), std::size_t{2}); + + for (int i = 0; i < 4; ++i) { + stepScenarioRuntime(runtime, 0.1); + } + + const auto& route = query.get(entity); + SC_EXPECT_EQ(route.destinationZoneId, std::string{"far-exit"}); + SC_EXPECT_TRUE(!route.noExitAvailable); + SC_EXPECT_TRUE(std::none_of( + route.waypointConnectionIds.begin(), + route.waypointConnectionIds.end(), + [](const auto& connectionId) { + return connectionId == "room-near-exit"; + })); + + const auto& exposure = + runtime.world().resources().get(); + SC_EXPECT_TRUE(exposure.hazardsById.at("combined-fire").exposedAgentSeconds > 0.0); + SC_EXPECT_TRUE(exposure.hazardsById.at("combined-smoke").exposedAgentSeconds > 0.0); + SC_EXPECT_EQ(exposure.hazardsById.at("combined-fire").peakExposedAgentCount, std::size_t{1}); + SC_EXPECT_EQ(exposure.hazardsById.at("combined-smoke").peakExposedAgentCount, std::size_t{1}); +} + SC_TEST(ScenarioRiskMetricsSystem_PublishesStalledHotspotAndBottleneckMetrics) { std::vector seeds; for (int index = 0; index < 5; ++index) {