use std::collections::HashMap;
use super::error::AssignmentError;
use super::indexed_graph::IndexedGraph;
use crate::gmns::meso::network::Network;
use crate::gmns::types::LinkID;
use crate::od::OdMatrix;
const EPS_BETA: f64 = 1e-12;
pub trait VolumeDelayFunction {
fn travel_time(&self, free_flow_time: f64, volume: f64, capacity: f64) -> f64;
fn integral(&self, free_flow_time: f64, volume: f64, capacity: f64) -> f64;
}
#[derive(Debug, Clone, Copy)]
pub struct BprFunction {
pub alpha: f64,
pub beta: f64,
beta_int: Option<u32>,
}
impl BprFunction {
pub fn new(alpha: f64, beta: f64) -> Self {
BprFunction {
alpha,
beta,
beta_int: Self::detect_integer_beta(beta),
}
}
fn detect_integer_beta(beta: f64) -> Option<u32> {
if beta < 0.0 || !beta.is_finite() {
return None;
}
let rounded = beta.round();
if (beta - rounded).abs() < EPS_BETA && rounded >= 1.0 && rounded <= 20.0 {
Some(rounded as u32)
} else {
None
}
}
}
impl Default for BprFunction {
fn default() -> Self {
BprFunction::new(0.15, 4.0)
}
}
impl BprFunction {
#[inline]
fn ratio_pow(&self, ratio: f64) -> f64 {
match self.beta_int {
Some(n) => ratio.powi(n as i32),
None => ratio.powf(self.beta),
}
}
#[inline]
fn ratio_pow_plus1(&self, ratio: f64) -> f64 {
match self.beta_int {
Some(n) => ratio.powi(n as i32 + 1),
None => ratio.powf(self.beta + 1.0),
}
}
}
impl VolumeDelayFunction for BprFunction {
fn travel_time(&self, free_flow_time: f64, volume: f64, capacity: f64) -> f64 {
if capacity <= 0.0 {
return f64::INFINITY;
}
free_flow_time * (1.0 + self.alpha * self.ratio_pow(volume / capacity))
}
fn integral(&self, free_flow_time: f64, volume: f64, capacity: f64) -> f64 {
if capacity <= 0.0 {
return f64::INFINITY;
}
if volume <= 0.0 {
return 0.0;
}
let ratio = volume / capacity;
free_flow_time
* (volume + self.alpha * capacity * self.ratio_pow_plus1(ratio) / (self.beta + 1.0))
}
}
#[derive(Debug, Clone)]
pub struct AssignmentConfig {
pub max_iterations: usize,
pub convergence_gap: f64,
}
impl Default for AssignmentConfig {
fn default() -> Self {
AssignmentConfig {
max_iterations: 100,
convergence_gap: 1e-4,
}
}
}
#[derive(Debug, Clone)]
pub struct AssignmentResult {
pub link_volumes: HashMap<LinkID, f64>,
pub link_costs: HashMap<LinkID, f64>,
pub iterations: usize,
pub relative_gap: f64,
pub converged: bool,
}
pub trait AssignmentMethod {
fn assign(
&self,
network: &Network,
graph: &IndexedGraph,
od_matrix: &dyn OdMatrix,
vdf: &dyn VolumeDelayFunction,
config: &AssignmentConfig,
) -> Result<AssignmentResult, AssignmentError>;
}
pub fn compute_link_costs(
network: &Network,
volumes: &HashMap<LinkID, f64>,
vdf: &dyn VolumeDelayFunction,
) -> HashMap<LinkID, f64> {
let mut costs = HashMap::with_capacity(network.links.len());
compute_link_costs_into(network, volumes, vdf, &mut costs);
costs
}
pub fn compute_link_costs_into(
network: &Network,
volumes: &HashMap<LinkID, f64>,
vdf: &dyn VolumeDelayFunction,
out: &mut HashMap<LinkID, f64>,
) {
for (&link_id, link) in &network.links {
let volume = volumes.get(&link_id).copied().unwrap_or(0.0);
let ff_time = link.get_free_flow_time_hours();
let capacity = link.get_total_capacity();
let cost = vdf.travel_time(ff_time, volume, capacity);
*out.entry(link_id).or_insert(0.0) = cost;
}
}
pub fn beckmann_objective(
network: &Network,
volumes: &HashMap<LinkID, f64>,
vdf: &dyn VolumeDelayFunction,
) -> f64 {
let mut objective = 0.0;
for (&link_id, link) in &network.links {
let volume = volumes.get(&link_id).copied().unwrap_or(0.0);
let ff_time = link.get_free_flow_time_hours();
let capacity = link.get_total_capacity();
objective += vdf.integral(ff_time, volume, capacity);
}
objective
}
pub fn compute_relative_gap(
volumes: &HashMap<LinkID, f64>,
costs: &HashMap<LinkID, f64>,
aux_volumes: &HashMap<LinkID, f64>,
) -> f64 {
let numerator: f64 = volumes
.iter()
.map(|(&link_id, &vol)| {
let cost = costs.get(&link_id).copied().unwrap_or(0.0);
vol * cost
})
.sum();
let denominator: f64 = aux_volumes
.iter()
.map(|(&link_id, &vol)| {
let cost = costs.get(&link_id).copied().unwrap_or(0.0);
vol * cost
})
.sum();
if numerator <= 0.0 {
return 0.0;
}
(numerator - denominator) / numerator
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-10;
#[test]
fn bpr_zero_volume_returns_free_flow() {
let bpr = BprFunction::default();
assert_eq!(bpr.travel_time(10.0, 0.0, 1000.0), 10.0);
}
#[test]
fn bpr_at_capacity() {
let bpr = BprFunction::default();
let t = bpr.travel_time(10.0, 1000.0, 1000.0);
assert!((t - 11.5).abs() < EPS);
}
#[test]
fn bpr_half_capacity() {
let bpr = BprFunction::default();
let t = bpr.travel_time(10.0, 500.0, 1000.0);
assert!((t - 10.09375).abs() < EPS);
}
#[test]
fn bpr_double_capacity() {
let bpr = BprFunction::default();
let t = bpr.travel_time(10.0, 2000.0, 1000.0);
assert!((t - 34.0).abs() < EPS);
}
#[test]
fn bpr_zero_capacity_returns_infinity() {
let bpr = BprFunction::default();
assert_eq!(bpr.travel_time(10.0, 100.0, 0.0), f64::INFINITY);
}
#[test]
fn bpr_negative_capacity_returns_infinity() {
let bpr = BprFunction::default();
assert_eq!(bpr.travel_time(10.0, 100.0, -500.0), f64::INFINITY);
}
#[test]
fn bpr_custom_params() {
let bpr = BprFunction::new(0.5, 2.0);
let t = bpr.travel_time(10.0, 500.0, 1000.0);
assert!((t - 11.25).abs() < EPS);
}
#[test]
fn bpr_integral_zero_volume() {
let bpr = BprFunction::default();
assert_eq!(bpr.integral(10.0, 0.0, 1000.0), 0.0);
}
#[test]
fn bpr_integral_at_capacity() {
let bpr = BprFunction::default();
let integral = bpr.integral(10.0, 1000.0, 1000.0);
assert!((integral - 10300.0).abs() < 1e-6);
}
#[test]
fn bpr_integral_zero_capacity_returns_infinity() {
let bpr = BprFunction::default();
assert_eq!(bpr.integral(10.0, 100.0, 0.0), f64::INFINITY);
}
#[test]
fn bpr_integral_monotonic() {
let bpr = BprFunction::default();
let i1 = bpr.integral(10.0, 500.0, 1000.0);
let i2 = bpr.integral(10.0, 1000.0, 1000.0);
let i3 = bpr.integral(10.0, 1500.0, 1000.0);
assert!(i1 < i2);
assert!(i2 < i3);
}
#[test]
fn relative_gap_equal_volumes_is_zero() {
let volumes = HashMap::from([(1, 100.0), (2, 200.0)]);
let costs = HashMap::from([(1, 5.0), (2, 10.0)]);
let gap = compute_relative_gap(&volumes, &costs, &volumes);
assert_eq!(gap, 0.0);
}
#[test]
fn relative_gap_known_value() {
let volumes = HashMap::from([(1, 100.0), (2, 50.0)]);
let costs = HashMap::from([(1, 2.0), (2, 4.0)]);
let aux = HashMap::from([(1, 120.0), (2, 30.0)]);
let gap = compute_relative_gap(&volumes, &costs, &aux);
assert!((gap - 0.1).abs() < EPS);
}
#[test]
fn relative_gap_zero_volumes_returns_zero() {
let volumes: HashMap<LinkID, f64> = HashMap::new();
let costs = HashMap::from([(1, 5.0)]);
let aux = HashMap::from([(1, 100.0)]);
let gap = compute_relative_gap(&volumes, &costs, &aux);
assert_eq!(gap, 0.0);
}
}