use rustsim_core::types::AgentId;
use rustsim_spaces::link::{LinkId, LinkSpace, LinkSpaceError};
use crate::{LinkProperties, SignalPhase, SignalTiming, TrafficControlType, TransportLinkOps};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SpeedConstraint {
Unconstrained,
Leader {
leader: AgentId,
gap_m: f64,
},
BlockedExit {
remaining_m: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SpeedDecision {
pub speed: f64,
pub constraint: SpeedConstraint,
}
pub trait QueuePolicy {
fn speed_for(
&self,
space: &LinkSpace<LinkProperties>,
agent: AgentId,
) -> Result<SpeedDecision, LinkSpaceError>;
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FifoGapPolicy {
pub min_gap_m: f64,
pub exit_clearance_m: f64,
}
impl Default for FifoGapPolicy {
fn default() -> Self {
Self {
min_gap_m: 5.0,
exit_clearance_m: 0.5,
}
}
}
impl FifoGapPolicy {
pub fn new(min_gap_m: f64, exit_clearance_m: f64) -> Self {
Self {
min_gap_m,
exit_clearance_m,
}
}
}
impl QueuePolicy for FifoGapPolicy {
fn speed_for(
&self,
space: &LinkSpace<LinkProperties>,
agent: AgentId,
) -> Result<SpeedDecision, LinkSpaceError> {
let (link_id, position) = space
.agent_position(agent)
.ok_or(LinkSpaceError::AgentNotFound(agent))?;
let link_speed = space.link_speed(link_id);
let ids = space.agent_ids_on_link(link_id);
let my_idx = ids.iter().position(|&id| id == agent).expect(
"invariant: agent_position returned link_id whose agent list must contain the agent",
);
let min_gap_m = self.min_gap_m.max(0.0);
let exit_clearance_m = self.exit_clearance_m.max(0.0);
if my_idx + 1 < ids.len() {
let leader = ids[my_idx + 1];
let leader_pos = space
.agent_position(leader)
.expect("invariant: leader came from the same link's agent list and must resolve")
.1;
let gap_m = leader_pos - position;
let safe_speed = (gap_m - min_gap_m).max(0.0);
return Ok(SpeedDecision {
speed: link_speed.min(safe_speed),
constraint: SpeedConstraint::Leader { leader, gap_m },
});
}
if space.link_exit_blocked(link_id) {
let remaining_m = space.link_length(link_id).unwrap_or(0.0) - position;
let safe_speed = (remaining_m - exit_clearance_m).max(0.0);
return Ok(SpeedDecision {
speed: link_speed.min(safe_speed),
constraint: SpeedConstraint::BlockedExit { remaining_m },
});
}
Ok(SpeedDecision {
speed: link_speed,
constraint: SpeedConstraint::Unconstrained,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct ControlContext<'a> {
pub sim_time_s: f64,
pub traffic_control: TrafficControlType,
pub signal_timing: Option<&'a SignalTiming>,
pub approach_link: Option<LinkId>,
}
impl<'a> ControlContext<'a> {
pub fn new(sim_time_s: f64, traffic_control: TrafficControlType) -> Self {
Self {
sim_time_s,
traffic_control,
signal_timing: None,
approach_link: None,
}
}
pub fn with_signal_timing(mut self, signal_timing: &'a SignalTiming) -> Self {
self.signal_timing = Some(signal_timing);
self
}
pub fn with_approach_link(mut self, approach_link: LinkId) -> Self {
self.approach_link = Some(approach_link);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ControlDecision {
Proceed,
Hold {
wait_s: f64,
},
}
impl ControlDecision {
pub fn can_proceed(self) -> bool {
matches!(self, Self::Proceed)
}
pub fn wait_s(self) -> f64 {
match self {
Self::Proceed => 0.0,
Self::Hold { wait_s } => wait_s,
}
}
}
pub trait ControlPolicy {
fn decide(&self, context: ControlContext<'_>) -> ControlDecision;
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FixedControlPolicy {
pub stop_delay_s: f64,
pub yield_delay_s: f64,
}
impl Default for FixedControlPolicy {
fn default() -> Self {
Self {
stop_delay_s: 0.0,
yield_delay_s: 0.0,
}
}
}
impl FixedControlPolicy {
pub fn new(stop_delay_s: f64, yield_delay_s: f64) -> Self {
Self {
stop_delay_s,
yield_delay_s,
}
}
}
impl ControlPolicy for FixedControlPolicy {
fn decide(&self, context: ControlContext<'_>) -> ControlDecision {
match context.traffic_control {
TrafficControlType::Uncontrolled => ControlDecision::Proceed,
TrafficControlType::Yield => hold_or_proceed(self.yield_delay_s),
TrafficControlType::Stop => hold_or_proceed(self.stop_delay_s),
TrafficControlType::Signal => match context.signal_timing {
Some(timing) => {
let (phase, remaining_s) = timing.phase_at(context.sim_time_s);
match phase {
SignalPhase::Green => ControlDecision::Proceed,
SignalPhase::Red => ControlDecision::Hold {
wait_s: remaining_s.max(0.0),
},
}
}
None => ControlDecision::Proceed,
},
}
}
}
fn hold_or_proceed(delay_s: f64) -> ControlDecision {
if delay_s > 0.0 {
ControlDecision::Hold { wait_s: delay_s }
} else {
ControlDecision::Proceed
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::LinkProperties;
fn one_link_space() -> (LinkSpace<LinkProperties>, LinkId) {
let mut space = LinkSpace::new();
let a = space.add_node();
let b = space.add_node();
let (geom, props) = LinkProperties::urban(100.0, 36.0, 1).unwrap();
let link = space.add_link(a, b, geom, props).unwrap();
(space, link)
}
#[test]
fn fifo_gap_policy_limits_trailing_agent_by_leader_gap() {
let (mut space, link) = one_link_space();
space.add_agent_to_link(1, link, 10.0).unwrap();
space.add_agent_to_link(2, link, 18.0).unwrap();
let decision = FifoGapPolicy::default().speed_for(&space, 1).unwrap();
assert_eq!(decision.speed, 3.0);
assert_eq!(
decision.constraint,
SpeedConstraint::Leader {
leader: 2,
gap_m: 8.0
}
);
}
#[test]
fn default_transport_agent_speed_uses_fifo_gap_policy() {
let (mut space, link) = one_link_space();
space.add_agent_to_link(1, link, 10.0).unwrap();
space.add_agent_to_link(2, link, 18.0).unwrap();
assert_eq!(space.agent_speed(1).unwrap(), 3.0);
assert_eq!(
space
.agent_speed_decision(1, &FifoGapPolicy::default())
.unwrap()
.speed,
3.0
);
}
#[test]
fn blocked_exit_policy_limits_last_agent() {
let (mut space, link) = one_link_space();
space.add_agent_to_link(1, link, 98.0).unwrap();
space.set_link_exit_blocked(link, true);
let decision = FifoGapPolicy::default().speed_for(&space, 1).unwrap();
assert_eq!(decision.speed, 1.5);
assert_eq!(
decision.constraint,
SpeedConstraint::BlockedExit { remaining_m: 2.0 }
);
}
#[test]
fn fixed_control_policy_blocks_red_signal_and_releases_green() {
let policy = FixedControlPolicy::default();
let timing = SignalTiming::new(60.0, 0.0, vec![30.0]);
let red = policy.decide(
ControlContext::new(45.0, TrafficControlType::Signal).with_signal_timing(&timing),
);
assert_eq!(red, ControlDecision::Hold { wait_s: 15.0 });
let green = policy.decide(
ControlContext::new(5.0, TrafficControlType::Signal).with_signal_timing(&timing),
);
assert_eq!(green, ControlDecision::Proceed);
}
}