Skip to content

Commit dc99d8b

Browse files
[Analysis] Add hazard closure regression coverage
1 parent d907532 commit dc99d8b

3 files changed

Lines changed: 118 additions & 0 deletions

File tree

docs/product/고급 위험 모델.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
- 관련 문서: [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/)
3838
- 여기서 바로 가져올 수 있는 것은 `시야 저하`, `친숙도`, `유도 신호`, `연기/FED 후처리`의 단계적 결합이다.
3939
- 반면 연기 중 독자 길찾기 알고리즘이나 세부 인지 심리 모델까지 Pathfinder가 정식 기능으로 명세하는 것은 아니다.
40+
- 현재 v2 구현은 화재/연기 authoring, 단순 반응, 노출 요약, replay 표시까지의 경량 범위다. FDS coupling, FED 계산, 화재 확산, 연기 농도/가시거리장 시뮬레이션은 아직 포함하지 않는다.
4041

4142
### 2.4. 검증 시나리오로 보는 고급 거동
4243
- Pathfinder는 merging, bidirectional flow, stair passing, elevator loading 같은 거동을 제품 기능 자체보다 `검증 시나리오`로 많이 다룬다.

docs/product/위험 정의.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ Pathfinder 2026.1 공식 문서를 다시 확인한 결과, 현재 제품 기능
6464

6565
- 즉, 이 문서의 네 위험 축은 제품 개념 모델로 유지하되, 각 축의 구현 깊이는 같은 단계로 올라가지 않는다.
6666

67+
### v2 구현 한계
68+
- v2의 화재/연기 기능은 시나리오 작성, 단순 반응, 경로 회피, 노출 요약, 결과 표시까지를 다루는 경량 모델이다.
69+
- FDS/PyroSim 출력과의 직접 coupling은 포함하지 않는다.
70+
- FED, 독성 가스 용량, 생리학적 피해 판정은 계산하지 않는다.
71+
- 화재 확산, 열 방출률 변화, 구역 간 전파는 시뮬레이션하지 않는다.
72+
- 연기 농도장이나 상세 가시거리장을 계산하지 않는다. 현재 연기 영향은 위치 기반 반경과 단순 속도/시야 저하 proxy로 제한한다.
73+
- Pathfinder 문서 기준의 visibility/FED/FDS 후처리 파이프라인은 `중기 확장` 또는 별도 issue 범위로 남긴다.
74+
6775
## 1. SafeCrowd 기본 위험 체계
6876

6977
초기 제품 문서에서는 위험을 아래 네 축으로 정의한다.

tests/ScenarioSimulationSystemsTests.cpp

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,30 @@ void addClosureMotionSystems(
516516
.triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame});
517517
}
518518

519+
void addHazardClosureMotionSystems(
520+
safecrowd::engine::EngineRuntime& runtime,
521+
const safecrowd::domain::FacilityLayout2D& layout,
522+
std::vector<safecrowd::domain::EnvironmentHazardDraft> hazards,
523+
std::vector<safecrowd::domain::ConnectionBlockDraft> blocks) {
524+
runtime.addSystem(
525+
safecrowd::domain::makeScenarioControlSystem(layout, std::move(blocks)),
526+
{.phase = safecrowd::engine::UpdatePhase::PreSimulation,
527+
.triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame});
528+
runtime.addSystem(
529+
safecrowd::domain::makeScenarioEnvironmentHazardSystem(layout, std::move(hazards)),
530+
{.phase = safecrowd::engine::UpdatePhase::PostSimulation,
531+
.order = -20,
532+
.triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame});
533+
runtime.addSystem(
534+
safecrowd::domain::makeScenarioSimulationMotionSystem(layout),
535+
{.phase = safecrowd::engine::UpdatePhase::PostSimulation,
536+
.triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame});
537+
runtime.addSystem(
538+
std::make_unique<safecrowd::domain::ScenarioFrameSyncSystem>(),
539+
{.phase = safecrowd::engine::UpdatePhase::RenderSync,
540+
.triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame});
541+
}
542+
519543
void stepScenarioRuntime(safecrowd::engine::EngineRuntime& runtime, double deltaSeconds) {
520544
runtime.world().resources().set(safecrowd::domain::ScenarioSimulationStepResource{.deltaSeconds = deltaSeconds});
521545
runtime.stepFrame(0.0);
@@ -2349,6 +2373,91 @@ SC_TEST(ScenarioSimulationMotionSystem_RetriesNoExitAfterFiniteDoorClosureReopen
23492373
SC_EXPECT_TRUE(routeRecovered);
23502374
}
23512375

2376+
SC_TEST(ScenarioSimulationMotionSystem_CombinesHazardExposureWithDoorClosureReroute) {
2377+
auto layout = wideTwoExitHazardRouteLayout();
2378+
safecrowd::domain::ConnectionBlockDraft block;
2379+
block.id = "block-near-exit";
2380+
block.connectionId = "room-near-exit";
2381+
2382+
auto seed = doorRouteSeed(
2383+
{.x = 8.4, .y = 1.0},
2384+
"near-exit",
2385+
"room-near-exit",
2386+
{{.x = 10.0, .y = 0.7}, {.x = 10.0, .y = 1.3}},
2387+
1.0,
2388+
0.2);
2389+
seed.agent.reactionDelaySeconds = 10.0;
2390+
seed.agent.hazardSensitivity = 1.0;
2391+
seed.agent.smokeSensitivity = 1.0;
2392+
2393+
auto fire = hazardDraft(
2394+
"combined-fire",
2395+
safecrowd::domain::EnvironmentHazardKind::Fire,
2396+
safecrowd::domain::ScenarioElementSeverity::Low,
2397+
{.x = 8.4, .y = 1.0},
2398+
"room");
2399+
auto smoke = hazardDraft(
2400+
"combined-smoke",
2401+
safecrowd::domain::EnvironmentHazardKind::Smoke,
2402+
safecrowd::domain::ScenarioElementSeverity::Low,
2403+
{.x = 8.6, .y = 1.0},
2404+
"room");
2405+
2406+
safecrowd::engine::EngineRuntime runtime({
2407+
.fixedDeltaTime = 0.1,
2408+
.maxCatchUpSteps = 1,
2409+
.baseSeed = 94,
2410+
});
2411+
runtime.addSystem(std::make_unique<safecrowd::domain::ScenarioAgentSpawnSystem>(
2412+
std::vector<safecrowd::domain::ScenarioAgentSeed>{seed},
2413+
5.0));
2414+
addHazardClosureMotionSystems(runtime, layout, {fire, smoke}, {block});
2415+
2416+
runtime.play();
2417+
stepScenarioRuntime(runtime, 0.1);
2418+
2419+
auto& query = runtime.world().query();
2420+
const auto entities = query.view<
2421+
safecrowd::domain::Position,
2422+
safecrowd::domain::Velocity,
2423+
safecrowd::domain::EvacuationRoute>();
2424+
SC_EXPECT_EQ(entities.size(), std::size_t{1});
2425+
const auto entity = entities.front();
2426+
2427+
const auto& firstState =
2428+
runtime.world().resources().get<safecrowd::domain::ScenarioEnvironmentReactionResource>().agentsById.at(entity.index);
2429+
SC_EXPECT_TRUE(firstState.hazardDetected);
2430+
SC_EXPECT_TRUE(!firstState.hazardAware);
2431+
SC_EXPECT_TRUE(firstState.closureDetected);
2432+
SC_EXPECT_TRUE(!firstState.closureAware);
2433+
SC_EXPECT_EQ(firstState.blockedConnectionId, std::string{"room-near-exit"});
2434+
2435+
const auto& activeHazards =
2436+
runtime.world().resources().get<safecrowd::domain::ScenarioActiveEnvironmentHazardsResource>();
2437+
SC_EXPECT_EQ(activeHazards.hazards.size(), std::size_t{2});
2438+
2439+
for (int i = 0; i < 4; ++i) {
2440+
stepScenarioRuntime(runtime, 0.1);
2441+
}
2442+
2443+
const auto& route = query.get<safecrowd::domain::EvacuationRoute>(entity);
2444+
SC_EXPECT_EQ(route.destinationZoneId, std::string{"far-exit"});
2445+
SC_EXPECT_TRUE(!route.noExitAvailable);
2446+
SC_EXPECT_TRUE(std::none_of(
2447+
route.waypointConnectionIds.begin(),
2448+
route.waypointConnectionIds.end(),
2449+
[](const auto& connectionId) {
2450+
return connectionId == "room-near-exit";
2451+
}));
2452+
2453+
const auto& exposure =
2454+
runtime.world().resources().get<safecrowd::domain::ScenarioHazardExposureResource>();
2455+
SC_EXPECT_TRUE(exposure.hazardsById.at("combined-fire").exposedAgentSeconds > 0.0);
2456+
SC_EXPECT_TRUE(exposure.hazardsById.at("combined-smoke").exposedAgentSeconds > 0.0);
2457+
SC_EXPECT_EQ(exposure.hazardsById.at("combined-fire").peakExposedAgentCount, std::size_t{1});
2458+
SC_EXPECT_EQ(exposure.hazardsById.at("combined-smoke").peakExposedAgentCount, std::size_t{1});
2459+
}
2460+
23522461
SC_TEST(ScenarioRiskMetricsSystem_PublishesStalledHotspotAndBottleneckMetrics) {
23532462
std::vector<safecrowd::domain::ScenarioAgentSeed> seeds;
23542463
for (int index = 0; index < 5; ++index) {

0 commit comments

Comments
 (0)