Skip to content
Open
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 src/domain/AgentComponents.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct Agent {
double hazardSensitivity{1.0};
double smokeSensitivity{1.0};
double reactionDelaySeconds{0.0};
double detectionDelaySeconds{0.0};
double closurePatienceSeconds{0.0};
};

Expand Down
3 changes: 3 additions & 0 deletions src/domain/ScenarioSimulationRunner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ ScenarioAgentSeed ScenarioSimulationRunner::createAgentSeed(
^ mix64(fnv1a64(sourcePlacementId))
^ mix64(fnv1a64(startZoneId))
^ mix64(static_cast<std::uint64_t>(evacuationRoute.destinationZoneId.size()));
const auto detectionSalt = mix64(propensitySalt ^ 0x9E3779B97F4A7C15ULL);
constexpr double kAgentDetectionDelayMaxSeconds = 3.0;
return {
.position = {.value = position},
.agent = {
Expand All @@ -282,6 +284,7 @@ ScenarioAgentSeed ScenarioSimulationRunner::createAgentSeed(
.sourcePlacementId = sourcePlacementId,
.sourceZoneId = startZoneId,
.guidancePropensity = beta22(baseSeed, propensitySalt),
.detectionDelaySeconds = kAgentDetectionDelayMaxSeconds * beta22(baseSeed, detectionSalt),
},
.velocity = {.value = {}},
.route = std::move(evacuationRoute),
Expand Down
34 changes: 29 additions & 5 deletions src/domain/ScenarioSimulationSystems.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,19 @@ class ScenarioControlSystem final : public engine::EngineSystem {
std::uint64_t revision_{0};
};

void resetHazardEncounter(ScenarioEnvironmentReactionAgentState& state) {
state.hazardDetected = false;
state.hazardAware = false;
state.hazardInRange = false;
state.hazardDistanceMeters = 0.0;
state.hazardRadiusMeters = 0.0;
state.hazardSpeedFactor = 1.0;
state.hazardRoutePenaltyMeters = 0.0;
state.hazardSensedSinceSeconds = 0.0;
state.hazardDetectedAtSeconds = 0.0;
state.hazardReactionReadySeconds = 0.0;
}

class ScenarioEnvironmentHazardSystem final : public engine::EngineSystem {
public:
ScenarioEnvironmentHazardSystem(FacilityLayout2D layout, std::vector<EnvironmentHazardDraft> hazards)
Expand Down Expand Up @@ -774,21 +787,32 @@ class ScenarioEnvironmentHazardSystem final : public engine::EngineSystem {

auto& state = reactions.agentsById[entity.index];
if (detectedHazard == nullptr) {
state.hazardInRange = false;
resetHazardEncounter(state);
continue;
}

if (!state.hazardDetected || state.hazardKey != detectedHazard->key) {
state.hazardDetected = true;
state.hazardAware = false;
if (!state.hazardInRange || state.hazardKey != detectedHazard->key) {
// New hazard encounter: restart the sense -> detect -> react pipeline.
state.hazardKey = detectedHazard->key;
state.hazardSensedSinceSeconds = elapsedSeconds;
state.hazardDetected = false;
state.hazardAware = false;
}

// Detection lags entering sensing range by the agent's detection delay;
// the reaction delay then runs from the moment of detection.
if (!state.hazardDetected
&& elapsedSeconds + 1e-9
>= state.hazardSensedSinceSeconds + std::max(0.0, agent.detectionDelaySeconds)) {
state.hazardDetected = true;
state.hazardDetectedAtSeconds = elapsedSeconds;
state.hazardReactionReadySeconds =
elapsedSeconds + std::max(0.0, agent.reactionDelaySeconds);
}

state.hazardInRange = true;
state.hazardAware = elapsedSeconds + 1e-9 >= state.hazardReactionReadySeconds;
state.hazardAware = state.hazardDetected
&& elapsedSeconds + 1e-9 >= state.hazardReactionReadySeconds;
state.hazardKind = detectedHazard->draft.kind;
state.hazardSeverity = detectedHazard->draft.severity;
state.hazardPosition = detectedHazard->draft.position;
Expand Down
1 change: 1 addition & 0 deletions src/domain/ScenarioSimulationSystems.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ struct ScenarioEnvironmentReactionAgentState {
double hazardRadiusMeters{0.0};
double hazardSpeedFactor{1.0};
double hazardRoutePenaltyMeters{0.0};
double hazardSensedSinceSeconds{0.0};
double hazardDetectedAtSeconds{0.0};
double hazardReactionReadySeconds{0.0};
bool closureDetected{false};
Expand Down
105 changes: 105 additions & 0 deletions tests/ScenarioSimulationSystemsTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2225,6 +2225,111 @@ SC_TEST(ScenarioEnvironmentHazardSystem_DelaysFireAvoidanceUntilReactionReady) {
SC_EXPECT_TRUE(awareFrame.agents.front().position.y < -0.01);
}

SC_TEST(ScenarioEnvironmentHazardSystem_DelaysDetectionUntilDetectionDelayElapses) {
auto seed = straightRouteSeed({.x = 0.0, .y = 0.0}, 1.0, 0.0);
seed.agent.detectionDelaySeconds = 1.0; // must sense the hazard for 1s before detecting it
seed.agent.reactionDelaySeconds = 0.0; // react immediately once detected

// A large radius keeps the agent inside the hazard while it moves along its route,
// so the test isolates the detection delay rather than range changes.
safecrowd::domain::EnvironmentHazardDraft fire;
fire.id = "fire-detect-delay";
fire.kind = safecrowd::domain::EnvironmentHazardKind::Fire;
fire.name = "Test hazard";
fire.affectedZoneId = "room";
fire.position = {.x = 0.0, .y = 0.4};
fire.startSeconds = 0.0;
fire.endSeconds = 0.0;
fire.severity = safecrowd::domain::ScenarioElementSeverity::High;
fire.radiusMeters = 100.0;

safecrowd::engine::EngineRuntime runtime({
.fixedDeltaTime = 1.0 / 30.0,
.maxCatchUpSteps = 1,
.baseSeed = 71,
});
runtime.addSystem(std::make_unique<safecrowd::domain::ScenarioAgentSpawnSystem>(
std::vector<safecrowd::domain::ScenarioAgentSeed>{seed}, 10.0));
addHazardMotionSystems(runtime, straightExitLayout(), {fire});

runtime.play();

// After 0.25s the hazard is sensed but the 1s detection delay has not elapsed.
runtime.world().resources().set(safecrowd::domain::ScenarioSimulationStepResource{.deltaSeconds = 0.25});
runtime.stepFrame(0.0);

const auto& earlyState =
runtime.world().resources().get<safecrowd::domain::ScenarioEnvironmentReactionResource>().agentsById.at(0);
SC_EXPECT_TRUE(earlyState.hazardInRange);
SC_EXPECT_TRUE(!earlyState.hazardDetected);
SC_EXPECT_TRUE(!earlyState.hazardAware);

// Advance well past the detection delay; detection fires and, with no reaction delay,
// awareness follows immediately.
for (int step = 0; step < 7; ++step) {
runtime.world().resources().set(safecrowd::domain::ScenarioSimulationStepResource{.deltaSeconds = 0.25});
runtime.stepFrame(0.0);
}

const auto& lateState =
runtime.world().resources().get<safecrowd::domain::ScenarioEnvironmentReactionResource>().agentsById.at(0);
SC_EXPECT_TRUE(lateState.hazardDetected);
SC_EXPECT_TRUE(lateState.hazardAware);
}

SC_TEST(ScenarioEnvironmentHazardSystem_RestartsDetectionDelayAfterLeavingHazardRange) {
auto seed = straightRouteSeed({.x = 0.0, .y = 0.0}, 0.0, 0.0);
seed.agent.detectionDelaySeconds = 1.0;
seed.agent.reactionDelaySeconds = 0.0;

auto fire = hazardDraft(
"fire-reentry-delay",
safecrowd::domain::EnvironmentHazardKind::Fire,
safecrowd::domain::ScenarioElementSeverity::High,
{.x = 0.0, .y = 0.0});
fire.radiusMeters = 0.5;

safecrowd::engine::EngineRuntime runtime({
.fixedDeltaTime = 1.0 / 30.0,
.maxCatchUpSteps = 1,
.baseSeed = 72,
});
runtime.addSystem(std::make_unique<safecrowd::domain::ScenarioAgentSpawnSystem>(
std::vector<safecrowd::domain::ScenarioAgentSeed>{seed}, 10.0));
addHazardMotionSystems(runtime, straightExitLayout(), {fire});

runtime.play();
for (int step = 0; step < 5; ++step) {
stepScenarioRuntime(runtime, 0.25);
}

const auto& detectedState =
runtime.world().resources().get<safecrowd::domain::ScenarioEnvironmentReactionResource>().agentsById.at(0);
SC_EXPECT_TRUE(detectedState.hazardDetected);
SC_EXPECT_TRUE(detectedState.hazardAware);

auto& query = runtime.world().query();
const auto entity = query.view<safecrowd::domain::Position>().front();
query.get<safecrowd::domain::Position>(entity).value = {.x = 5.0, .y = 0.0};
stepScenarioRuntime(runtime, 0.25);

const auto& outsideState =
runtime.world().resources().get<safecrowd::domain::ScenarioEnvironmentReactionResource>().agentsById.at(0);
SC_EXPECT_TRUE(!outsideState.hazardInRange);
SC_EXPECT_TRUE(!outsideState.hazardDetected);
SC_EXPECT_TRUE(!outsideState.hazardAware);
SC_EXPECT_EQ(outsideState.hazardKey, std::string{"fire-reentry-delay"});

query.get<safecrowd::domain::Position>(entity).value = {.x = 0.0, .y = 0.0};
stepScenarioRuntime(runtime, 0.25);

const auto& reenteredState =
runtime.world().resources().get<safecrowd::domain::ScenarioEnvironmentReactionResource>().agentsById.at(0);
SC_EXPECT_TRUE(reenteredState.hazardInRange);
SC_EXPECT_TRUE(!reenteredState.hazardDetected);
SC_EXPECT_TRUE(!reenteredState.hazardAware);
}

SC_TEST(ScenarioEnvironmentHazardSystem_SmokeSlowsButDoesNotStopAgent) {
std::vector<safecrowd::domain::ScenarioAgentSeed> baselineSeeds;
baselineSeeds.push_back(straightRouteSeed({.x = 0.0, .y = 0.0}, 1.0));
Expand Down
Loading