Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/product/고급 위험 모델.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 같은 거동을 제품 기능 자체보다 `검증 시나리오`로 많이 다룬다.
Expand Down
8 changes: 8 additions & 0 deletions docs/product/위험 정의.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ Pathfinder 2026.1 공식 문서를 다시 확인한 결과, 현재 제품 기능

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

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

## 1. SafeCrowd 기본 위험 체계

초기 제품 문서에서는 위험을 아래 네 축으로 정의한다.
Expand Down
109 changes: 109 additions & 0 deletions tests/ScenarioSimulationSystemsTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,30 @@ void addClosureMotionSystems(
.triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame});
}

void addHazardClosureMotionSystems(
safecrowd::engine::EngineRuntime& runtime,
const safecrowd::domain::FacilityLayout2D& layout,
std::vector<safecrowd::domain::EnvironmentHazardDraft> hazards,
std::vector<safecrowd::domain::ConnectionBlockDraft> 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<safecrowd::domain::ScenarioFrameSyncSystem>(),
{.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);
Expand Down Expand Up @@ -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<safecrowd::domain::ScenarioAgentSpawnSystem>(
std::vector<safecrowd::domain::ScenarioAgentSeed>{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<safecrowd::domain::ScenarioEnvironmentReactionResource>().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<safecrowd::domain::ScenarioActiveEnvironmentHazardsResource>();
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<safecrowd::domain::EvacuationRoute>(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<safecrowd::domain::ScenarioHazardExposureResource>();
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<safecrowd::domain::ScenarioAgentSeed> seeds;
for (int index = 0; index < 5; ++index) {
Expand Down
Loading