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
16 changes: 16 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ add_library(safecrowd_domain STATIC
src/domain/GeometryQueries.h
src/domain/GeometryQueries.cpp
src/domain/PopulationSpec.h
src/domain/AlternativeRecommendationService.h
src/domain/AlternativeRecommendationService.cpp
src/domain/ScenarioAuthoring.h
src/domain/ScenarioAuthoring.cpp
src/domain/ScenarioBatchRunner.h
Expand Down Expand Up @@ -158,6 +160,7 @@ if (BUILD_TESTING)
tests/EngineIntegrationTests.cpp
tests/ResourceStoreTests.cpp
tests/DeterministicRngTests.cpp
tests/AlternativeRecommendationServiceTests.cpp
tests/ScenarioSimulationRunnerTests.cpp
tests/ScenarioAuthoringTests.cpp
tests/ScenarioBatchRunnerTests.cpp
Expand All @@ -174,6 +177,19 @@ if (BUILD_TESTING)
safecrowd_domain
)

if (SAFECROWD_BUILD_APP)
target_sources(safecrowd_tests
PRIVATE
tests/ProjectPersistenceTests.cpp
src/application/ProjectPersistence.cpp
)

target_link_libraries(safecrowd_tests
PRIVATE
Qt6::Core
)
endif()

configure_project_target(safecrowd_tests)

add_test(NAME safecrowd_tests COMMAND safecrowd_tests)
Expand Down
5 changes: 4 additions & 1 deletion docs/UI.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,9 @@ stage된 baseline 시나리오를 실제로 실행하고 진행 상태를 보는
- 하단 graph panel에 Remaining, Exits, Compare 탭 제공
- Exits 탭에 출구별 이용 인원, 비율, 마지막 통과 시각 표시
- Compare 탭은 v1 placeholder이며 baseline/alternative 비교 계산은 후속 Analysis Workspace 범위
- Batch Result 우측 패널에 완료 결과 아티팩트 기반 Recommendations v1 제공
- 차단 해제, 출구 분산 유도, 병목/압박 hotspot 완화 후보를 표시
- `Create Recommended Scenario`는 `Recommended` 시나리오 초안만 만들고 자동 재실행하지 않음
- 상세 탭에 Zones, Groups, Criteria 표시
- Zones 탭에 구역별 초기 인원, 대피 인원, 마지막 완료시각 표시
- Groups 탭에 배치 그룹별 초기 인원, 대피 인원, 마지막 완료시각 표시
Expand Down Expand Up @@ -554,7 +557,7 @@ stage된 baseline 시나리오를 실제로 실행하고 진행 상태를 보는
- [ ] Variation Summary 제공
- [ ] Heatmap Selector 제공
- [ ] Comparison View 제공
- [ ] Recommendation Drawer 제공
- [x] Recommendation Drawer v1 제공
- [ ] Export Dialog 제공

## 7. 문서 유지 규칙
Expand Down
2 changes: 0 additions & 2 deletions docs/alternative-recommendation-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@


---




Expand Down Expand Up @@ -189,4 +188,3 @@
- “문헌상 특정 조건에서 개선 사례가 보고됨”
- “현재 시나리오에서도 효과가 있는지는 재시뮬레이션으로 검증 필요”
- “장애물/분리대는 잘못 배치하면 오히려 악화될 수 있음”

13 changes: 4 additions & 9 deletions docs/alternative-recommendation-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,20 @@
- 판단기준
측정 구역(출구 앞 2M)에 밀도가 2명/㎡ 이상있을때,
10초 이상 대기 상태가 지속되고,



- 복도에서 병목
- 판단기준
복도 실제 통과 시간이 정상 예상 통과 시간의 2배 이상이고,
평균속도가 0.5이하 상태가 10초 이상 지속되면 복도 병목으로 판단한다.



- 제한시간 초과/ 미대피
- 판단기준
제한시간동안 모든인원이 대피하지 못했을때.


- 양방향 흐름 충돌
- 판단기준
120도 이상 반대 방향 흐름, 양쪽 인원 각각 30% 이상, 속도 0.7m/s 이하가 10초 이상



### 대안추천 예상값
Expand Down Expand Up @@ -69,5 +66,3 @@
|제한시간 초과 / 미대피 증가|과부하 출구 인원을 다른 출구로 분산|가장 혼잡한 E1 구역 지연시간 180초 감소|
|제한시간 초과 / 미대피 증가|여유 구역 E2/E3로 부하 이전|E2/E3 시간 증가는 30초 내외로 제한|
|제한시간 초과 / 미대피 증가|단계적 출발 / 지연 대기 전략|병목 전방 동시 압력 감소, 미대피 발생 억제|


44 changes: 30 additions & 14 deletions src/application/ScenarioAuthoringWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,23 @@ QLabel* createRoleBadge(const QString& text, bool alternative, QWidget* parent)
return badge;
}

QString scenarioRoleLabel(safecrowd::domain::ScenarioRole role) {
switch (role) {
case safecrowd::domain::ScenarioRole::Baseline:
return "Baseline";
case safecrowd::domain::ScenarioRole::Recommended:
return "Recommended";
case safecrowd::domain::ScenarioRole::Alternative:
default:
return "Alternative";
}
}

bool scenarioRoleHasBaselineDiff(safecrowd::domain::ScenarioRole role) {
return role == safecrowd::domain::ScenarioRole::Alternative
|| role == safecrowd::domain::ScenarioRole::Recommended;
}

void addMetaRow(QVBoxLayout* layout, const QString& label, const QString& value, QWidget* parent) {
auto* row = new QWidget(parent);
auto* rowLayout = new QHBoxLayout(row);
Expand Down Expand Up @@ -1223,7 +1240,7 @@ void ScenarioAuthoringWidget::createScenarioWithName(const QString& name, int so
scenario.crowdPlacements = source.crowdPlacements;
scenario.startText = source.startText;
scenario.destinationText = source.destinationText;
scenario.baseScenarioId = source.draft.role == safecrowd::domain::ScenarioRole::Alternative
scenario.baseScenarioId = scenarioRoleHasBaselineDiff(source.draft.role)
? source.baseScenarioId
: QString::fromStdString(source.draft.scenarioId);
scenario.stagedForRun = false;
Expand Down Expand Up @@ -1356,10 +1373,10 @@ void ScenarioAuthoringWidget::refreshInspector() {
if (!hasScenario) {
addStatusMessage(panelLayout, "No scenario selected", scenarioOverviewPanel_);
} else {
const bool alternative = scenario->draft.role == safecrowd::domain::ScenarioRole::Alternative;
const bool variation = scenarioRoleHasBaselineDiff(scenario->draft.role);
panelLayout->addWidget(createRoleBadge(
alternative ? "Alternative" : "Baseline",
alternative,
scenarioRoleLabel(scenario->draft.role),
variation,
scenarioOverviewPanel_));

auto* nameLabel = createLabel(
Expand All @@ -1377,7 +1394,7 @@ void ScenarioAuthoringWidget::refreshInspector() {
addMetaRow(panelLayout, "Blocked", QString::number(static_cast<int>(scenario->draft.control.connectionBlocks.size())), scenarioOverviewPanel_);
addMetaRow(panelLayout, "Start", scenario->startText, scenarioOverviewPanel_);
addMetaRow(panelLayout, "Destination", scenario->destinationText, scenarioOverviewPanel_);
if (alternative && !scenario->baseScenarioId.isEmpty()) {
if (variation && !scenario->baseScenarioId.isEmpty()) {
addMetaRow(panelLayout, "Based on", scenario->baseScenarioId, scenarioOverviewPanel_);
}
}
Expand All @@ -1397,7 +1414,7 @@ void ScenarioAuthoringWidget::refreshInspector() {
} else if (scenario->draft.role == safecrowd::domain::ScenarioRole::Baseline) {
addStatusMessage(panelLayout, "Baseline scenario", scenarioDiffPanel_);
} else if (scenario->baseScenarioId.isEmpty()) {
addStatusMessage(panelLayout, "Alternative scenario / no baseline link", scenarioDiffPanel_);
addStatusMessage(panelLayout, "Variation scenario / no baseline link", scenarioDiffPanel_);
} else {
const auto baseId = scenario->baseScenarioId.toStdString();
const auto baselineIt = std::find_if(scenarios_.begin(), scenarios_.end(), [&](const auto& candidate) {
Expand Down Expand Up @@ -1452,8 +1469,8 @@ void ScenarioAuthoringWidget::refreshInspector() {
if (!stagedScenario.stagedForRun || !scenarioHasOccupants(stagedScenario)) {
continue;
}
const auto role = stagedScenario.draft.role == safecrowd::domain::ScenarioRole::Baseline ? "Baseline" : "Alternative";
lines << QString("- %1 (%2)").arg(QString::fromStdString(stagedScenario.draft.name), role);
lines << QString("- %1 (%2)")
.arg(QString::fromStdString(stagedScenario.draft.name), scenarioRoleLabel(stagedScenario.draft.role));
}
}
stagedScenariosLabel_->setText(lines.join('\n'));
Expand Down Expand Up @@ -1698,8 +1715,8 @@ void ScenarioAuthoringWidget::refreshScenarioSwitcher() {
scenarioSwitcher_->blockSignals(true);
scenarioSwitcher_->clear();
for (const auto& scenario : scenarios_) {
const auto role = scenario.draft.role == safecrowd::domain::ScenarioRole::Baseline ? "Baseline" : "Alternative";
scenarioSwitcher_->addItem(QString("%1 (%2)").arg(QString::fromStdString(scenario.draft.name), role));
scenarioSwitcher_->addItem(QString("%1 (%2)")
.arg(QString::fromStdString(scenario.draft.name), scenarioRoleLabel(scenario.draft.role)));
}
scenarioSwitcher_->setCurrentIndex(currentScenarioIndex_);
scenarioSwitcher_->blockSignals(false);
Expand Down Expand Up @@ -1822,8 +1839,7 @@ void ScenarioAuthoringWidget::recomputeDependentVariationDiffKeys(const QString&
}

void ScenarioAuthoringWidget::recomputeVariationDiffKeysIfAlternative(ScenarioState& scenario) const {
if (scenario.draft.role != safecrowd::domain::ScenarioRole::Alternative
|| scenario.baseScenarioId.isEmpty()) {
if (!scenarioRoleHasBaselineDiff(scenario.draft.role) || scenario.baseScenarioId.isEmpty()) {
scenario.draft.variationDiffKeys.clear();
return;
}
Expand Down Expand Up @@ -2009,8 +2025,8 @@ QWidget* ScenarioAuthoringWidget::createScenarioPanel() {
if (!scenario.stagedForRun || !scenarioHasOccupants(scenario)) {
continue;
}
const auto role = scenario.draft.role == safecrowd::domain::ScenarioRole::Baseline ? "Baseline" : "Alternative";
lines << QString("- %1 (%2)").arg(QString::fromStdString(scenario.draft.name), role);
lines << QString("- %1 (%2)")
.arg(QString::fromStdString(scenario.draft.name), scenarioRoleLabel(scenario.draft.role));
}
}
stagedScenariosLabel_->setText(lines.join('\n'));
Expand Down
Loading
Loading