use std::num::NonZeroU64;
use std::sync::Arc;
use crate::model::constraint::multimodal::Constraint;
use crate::model::constraint::multimodal::{ConstraintConfig, MultimodalConstraintEngine};
use bambam_core::model::state::{
multimodal_state_ops as state_ops, LegIdx, MultimodalMapping, MultimodalStateMapping,
};
use routee_compass_core::model::traversal::EdgeFrontierContext;
use routee_compass_core::model::{
constraint::{ConstraintModel, ConstraintModelError},
network::Edge,
state::{StateModel, StateVariable},
};
pub struct MultimodalConstraintModel {
pub engine: Arc<MultimodalConstraintEngine>,
pub constraints: Vec<Constraint>,
pub max_trip_legs: NonZeroU64,
}
impl MultimodalConstraintModel {
pub fn new(
engine: Arc<MultimodalConstraintEngine>,
constraints: Vec<Constraint>,
max_trip_legs: NonZeroU64,
) -> Self {
Self {
engine,
constraints,
max_trip_legs,
}
}
pub fn new_local(
mode: &str,
constraints: Vec<Constraint>,
max_trip_legs: NonZeroU64,
modes: &[&str],
) -> Result<Self, ConstraintModelError> {
let mode_to_state =
MultimodalMapping::new(&modes.iter().map(|s| s.to_string()).collect::<Vec<String>>())
.map_err(|e| {
ConstraintModelError::BuildError(format!(
"while building local MultimodalConstraintModel, failure constructing mode mapping: {e}"
))
})?;
let engine = MultimodalConstraintEngine {
mode: mode.to_string(),
mode_to_state: Arc::new(mode_to_state),
};
let mmm = MultimodalConstraintModel::new(Arc::new(engine), constraints, max_trip_legs);
Ok(mmm)
}
}
impl ConstraintModel for MultimodalConstraintModel {
fn valid_frontier(
&self,
ctx: &EdgeFrontierContext,
state: &[StateVariable],
state_model: &StateModel,
) -> Result<bool, ConstraintModelError> {
validate_frontier(ctx.edge, state, state_model, self)
}
fn valid_edge(&self, _edge: &Edge) -> Result<bool, ConstraintModelError> {
Ok(true)
}
}
fn validate_frontier(
edge: &Edge,
state: &[StateVariable],
state_model: &StateModel,
model: &MultimodalConstraintModel,
) -> Result<bool, ConstraintModelError> {
let valid_leg_count = state_ops::appending_edge_mode_is_valid(
state,
state_model,
&model.engine.mode,
model.max_trip_legs,
&model.engine.mode_to_state,
)
.map_err(|e| {
let msg = format!("in multimodal constraint model, {e}");
ConstraintModelError::ConstraintModelError(msg)
})?;
if !valid_leg_count {
return Ok(false);
}
for constraint in model.constraints.iter() {
let valid = constraint.valid_frontier(
&model.engine.mode,
edge,
state,
state_model,
&model.engine.mode_to_state,
model.max_trip_legs,
)?;
log::debug!(
"multimodal frontier is valid? '{valid}' for edge {:?} with active_leg {}, trip_time: {:.2} minutes",
(edge.edge_list_id, edge.edge_id),
state_ops::get_active_leg_idx(state, state_model).unwrap_or_default().unwrap_or_default(),
state_model
.get_time(state, "trip_time")
.unwrap_or_default()
.get::<uom::si::time::minute>(),
);
if !valid {
return Ok(false);
}
}
Ok(true)
}
#[cfg(test)]
mod test {
use std::{
collections::{HashMap, HashSet},
num::NonZeroU64,
};
use itertools::Itertools;
use routee_compass_core::model::{
constraint::ConstraintModel,
network::Edge,
state::{StateModel, StateVariable},
traversal::TraversalModel,
};
use uom::si::f64::Length;
use crate::model::{
constraint::multimodal::{
model::{validate_frontier, MultimodalConstraintModel},
sequence_trie::SubSequenceTrie,
Constraint,
},
traversal::multimodal::MultimodalTraversalModel,
};
use bambam_core::model::state::{multimodal_state_ops as state_ops, MultimodalStateMapping};
#[test]
fn test_valid_max_trip_legs_empty_state() {
let max_trip_legs = NonZeroU64::new(1).unwrap();
let (mam, mfm, state_model, state) =
test_setup(vec![], "walk", &["walk", "bike"], max_trip_legs);
let edge = Edge::new(0, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
let is_valid = validate_frontier(&edge, &state, &state_model, &mfm).expect("test failed");
assert!(is_valid);
}
#[test]
fn test_valid_n_legs() {
let max_trip_legs = NonZeroU64::new(2).unwrap();
let (mam, mfm, state_model, mut state) =
test_setup(vec![], "walk", &["walk", "bike"], max_trip_legs);
let edge = Edge::new(0, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
state_ops::set_leg_mode(&mut state, 0, "walk", &state_model, &mam.mode_enumeration)
.expect("test invariant failed");
state_ops::increment_active_leg_idx(&mut state, &state_model, max_trip_legs)
.expect("test invariant failed");
let is_valid = validate_frontier(&edge, &state, &state_model, &mfm).expect("test failed");
assert!(is_valid);
}
#[test]
fn test_invalid_n_legs() {
let max_trip_legs = NonZeroU64::new(2).unwrap();
let (mam, mfm, state_model, mut state) =
test_setup(vec![], "walk", &["walk", "bike"], max_trip_legs);
let edge = Edge::new(0, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
inject_trip_legs(
&["walk", "bike"],
&mut state,
&state_model,
&mam.mode_enumeration,
max_trip_legs,
);
let is_valid = validate_frontier(&edge, &state, &state_model, &mfm).expect("test failed");
assert!(!is_valid);
}
#[test]
fn test_valid_mode_counts() {
let max_trip_legs = NonZeroU64::new(5).unwrap();
let mode_constraint = Constraint::ModeCounts(HashMap::from([
("walk".to_string(), 2),
("drive".to_string(), 1),
]));
let (mam, mfm, state_model, mut state) = test_setup(
vec![mode_constraint],
"walk",
&["walk", "bike", "drive", "tnc", "transit"],
max_trip_legs,
);
inject_trip_legs(
&["walk", "drive", "walk"],
&mut state,
&state_model,
&mam.mode_enumeration,
max_trip_legs,
);
let walk_edge_list = 0;
let edge = Edge::new(
walk_edge_list,
0,
0,
1,
Length::new::<uom::si::length::meter>(1000.0),
);
let is_valid = validate_frontier(&edge, &state, &state_model, &mfm).expect("test failed");
assert!(is_valid);
}
#[test]
fn test_invalid_mode_counts() {
let max_trip_legs = NonZeroU64::new(5).unwrap();
let mode_constraint = Constraint::ModeCounts(HashMap::from([
("walk".to_string(), 2),
("drive".to_string(), 1),
]));
let (mam, mfm, state_model, mut state) = test_setup(
vec![mode_constraint],
"walk",
&["walk", "bike", "drive", "tnc", "transit"],
max_trip_legs,
);
inject_trip_legs(
&["walk", "bike", "walk", "drive"],
&mut state,
&state_model,
&mam.mode_enumeration,
max_trip_legs,
);
let edge = Edge::new(0, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
let is_valid = validate_frontier(&edge, &state, &state_model, &mfm).expect("test failed");
assert!(!is_valid);
}
#[test]
fn test_valid_allowed_modes() {
let mode_constraint =
Constraint::AllowedModes(HashSet::from(["walk".to_string(), "transit".to_string()]));
let max_trip_legs = NonZeroU64::new(3).unwrap();
let (mam, mfm, state_model, mut state) = test_setup(
vec![mode_constraint],
"walk",
&["walk", "bike", "drive", "tnc", "transit"],
max_trip_legs,
);
inject_trip_legs(
&["walk", "transit", "walk"],
&mut state,
&state_model,
&mam.mode_enumeration,
max_trip_legs,
);
let walk_edge_list = 0;
let edge = Edge::new(
walk_edge_list,
0,
0,
1,
Length::new::<uom::si::length::meter>(1000.0),
);
let is_valid = validate_frontier(&edge, &state, &state_model, &mfm).expect("test failed");
assert!(is_valid);
}
#[test]
fn test_invalid_allowed_modes() {
let mode_constraint =
Constraint::AllowedModes(HashSet::from(["walk".to_string(), "transit".to_string()]));
let max_trip_legs = NonZeroU64::new(4).unwrap();
let (mtm, mfm, state_model, mut state) = test_setup(
vec![mode_constraint],
"drive",
&["walk", "bike", "drive", "tnc", "transit"],
max_trip_legs,
);
inject_trip_legs(
&["walk", "transit", "walk"],
&mut state,
&state_model,
&mtm.mode_enumeration,
max_trip_legs,
);
let edge = Edge::new(2, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
let is_valid = validate_frontier(&edge, &state, &state_model, &mfm).expect("test failed");
assert!(!is_valid);
}
#[test]
fn test_valid_subsequence_empty_state() {
let mut trie = SubSequenceTrie::new();
trie.insert_sequence(vec![
"walk".to_string(),
"transit".to_string(),
"walk".to_string(),
]);
let mode_constraint = Constraint::ExactSequences(trie);
let max_trip_legs = NonZeroU64::new(3).unwrap();
let (mam, mfm, state_model, mut state) = test_setup(
vec![mode_constraint],
"walk",
&["walk", "bike", "drive", "tnc", "transit"],
max_trip_legs,
);
let walk_edge_list = 0;
let edge = Edge::new(
walk_edge_list,
0,
0,
1,
Length::new::<uom::si::length::meter>(1000.0),
);
let is_valid = validate_frontier(&edge, &state, &state_model, &mfm).expect("test failed");
assert!(is_valid);
}
#[test]
fn test_valid_subsequence() {
let mut trie = SubSequenceTrie::new();
trie.insert_sequence(vec![
"walk".to_string(),
"transit".to_string(),
"walk".to_string(),
]);
let mode_constraint = Constraint::ExactSequences(trie);
let max_trip_legs = NonZeroU64::new(3).unwrap();
let (mam, mfm, state_model, mut state) = test_setup(
vec![mode_constraint],
"walk",
&["walk", "bike", "drive", "tnc", "transit"],
max_trip_legs,
);
inject_trip_legs(
&["walk", "transit"],
&mut state,
&state_model,
&mam.mode_enumeration,
max_trip_legs,
);
let walk_edge_list = 0;
let edge = Edge::new(
walk_edge_list,
0,
0,
1,
Length::new::<uom::si::length::meter>(1000.0),
);
let is_valid = validate_frontier(&edge, &state, &state_model, &mfm).expect("test failed");
assert!(is_valid);
}
#[test]
fn test_invalid_subsequence() {
let mut trie = SubSequenceTrie::new();
trie.insert_sequence(vec!["walk".to_string(), "transit".to_string()]);
let mode_constraint = Constraint::ExactSequences(trie);
let max_trip_legs = NonZeroU64::new(3).unwrap();
let (mam, mfm, state_model, mut state) = test_setup(
vec![mode_constraint],
"walk",
&["walk", "bike", "drive", "tnc", "transit"],
max_trip_legs,
);
let edge = Edge::new(1, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
inject_trip_legs(
&["walk", "transit"],
&mut state,
&state_model,
&mam.mode_enumeration,
max_trip_legs,
);
let is_valid = validate_frontier(&edge, &state, &state_model, &mfm).expect("test failed");
assert!(!is_valid);
}
fn test_setup(
constraints: Vec<Constraint>,
this_mode: &str,
modes: &[&str],
max_trip_legs: NonZeroU64,
) -> (
MultimodalTraversalModel,
MultimodalConstraintModel,
StateModel,
Vec<StateVariable>,
) {
let mtm = MultimodalTraversalModel::new_local(this_mode, max_trip_legs, modes)
.expect("test invariant failed");
let state_model = StateModel::new(mtm.output_features());
let mfm =
MultimodalConstraintModel::new_local(this_mode, constraints, max_trip_legs, modes)
.expect("test invariant failed");
let state = state_model
.initial_state(None)
.expect("test invariant failed");
(mtm, mfm, state_model, state)
}
fn inject_trip_legs(
legs: &[&str],
state: &mut [StateVariable],
state_model: &StateModel,
mode_to_state: &MultimodalStateMapping,
max_trip_legs: NonZeroU64,
) {
for (leg_idx, mode) in legs.iter().enumerate() {
state_ops::set_leg_mode(state, leg_idx as u64, mode, state_model, mode_to_state)
.expect("test invariant failed");
state_ops::increment_active_leg_idx(state, state_model, max_trip_legs)
.expect("test invariant failed");
}
}
#[test]
fn test_mode_counts_zero_limit() {
let mode_constraint = Constraint::ModeCounts(HashMap::from([
("walk".to_string(), 0),
("bike".to_string(), 1),
]));
let max_trip_legs = NonZeroU64::new(2).unwrap();
let (walk_mtm, walk_mfm, state_model, state) = test_setup(
vec![mode_constraint],
"walk", &["walk", "bike"],
max_trip_legs,
);
let walk_edge = Edge::new(0, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
let is_valid =
validate_frontier(&walk_edge, &state, &state_model, &walk_mfm).expect("test failed");
assert!(!is_valid);
}
#[test]
fn test_mode_counts_mode_not_in_limits() {
let mode_constraint = Constraint::ModeCounts(HashMap::from([
("walk".to_string(), 2),
("bike".to_string(), 1),
]));
let max_trip_legs = NonZeroU64::new(3).unwrap();
let (mam, mfm, state_model, state) = test_setup(
vec![mode_constraint],
"drive",
&["walk", "bike", "drive"], max_trip_legs,
);
let dummy_edge = Edge::new(2, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
let is_valid =
validate_frontier(&dummy_edge, &state, &state_model, &mfm).expect("test failed");
assert!(!is_valid);
}
#[test]
fn test_mode_counts_same_mode_continuation() {
let mode_constraint = Constraint::ModeCounts(HashMap::from([("walk".to_string(), 1)]));
let max_trip_legs = NonZeroU64::new(2).unwrap();
let (mam, mfm, state_model, mut state) = test_setup(
vec![mode_constraint],
"walk",
&["walk", "bike"],
max_trip_legs,
);
inject_trip_legs(
&["walk"],
&mut state,
&state_model,
&mam.mode_enumeration,
max_trip_legs,
);
let walk_edge = Edge::new(0, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
let is_valid =
validate_frontier(&walk_edge, &state, &state_model, &mfm).expect("test failed");
assert!(is_valid);
}
#[test]
fn test_allowed_modes_empty_set() {
let mode_constraint = Constraint::AllowedModes(HashSet::new());
let max_trip_legs = NonZeroU64::new(2).unwrap();
let modes = [
"walk", "bike", "drive", "tnc", "transit", "eBike", "eVTOL", "airplane", "ferry",
];
let (mam, mfm, state_model, state) =
test_setup(vec![mode_constraint], "walk", &modes, max_trip_legs);
for edge_list_id in (0..modes.len()) {
let edge = Edge::new(
edge_list_id,
0,
0,
1,
Length::new::<uom::si::length::meter>(1000.0),
);
let is_valid =
validate_frontier(&edge, &state, &state_model, &mfm).expect("test failed");
assert!(!is_valid);
}
}
#[test]
fn test_allowed_modes_case_sensitivity() {
let mode_constraint = Constraint::AllowedModes(HashSet::from([
"Walk".to_string(), ]));
let max_trip_legs = NonZeroU64::new(2).unwrap();
let (mam, mfm, state_model, state) = test_setup(
vec![mode_constraint],
"walk", &["walk", "Walk"], max_trip_legs,
);
let walk_edge = Edge::new(0, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0)); let is_valid =
validate_frontier(&walk_edge, &state, &state_model, &mfm).expect("test failed");
assert!(!is_valid); }
#[test]
fn test_exact_sequences_multiple_valid_sequences() {
let mut trie = SubSequenceTrie::new();
trie.insert_sequence(vec!["walk".to_string(), "transit".to_string()]);
trie.insert_sequence(vec!["bike".to_string(), "walk".to_string()]);
let mode_constraint = Constraint::ExactSequences(trie);
let max_trip_legs = NonZeroU64::new(3).unwrap();
let (mam, mfm, state_model, mut state) = test_setup(
vec![mode_constraint],
"walk",
&["walk", "bike", "transit"],
max_trip_legs,
);
inject_trip_legs(
&["bike"],
&mut state,
&state_model,
&mam.mode_enumeration,
max_trip_legs,
);
let walk_edge = Edge::new(0, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
let is_valid =
validate_frontier(&walk_edge, &state, &state_model, &mfm).expect("test failed");
assert!(is_valid);
}
#[test]
fn test_exact_sequences_empty_trie() {
let trie = SubSequenceTrie::new();
let mode_constraint = Constraint::ExactSequences(trie);
let max_trip_legs = NonZeroU64::new(2).unwrap();
let (mam, mfm, state_model, state) = test_setup(
vec![mode_constraint],
"walk",
&["walk", "bike"],
max_trip_legs,
);
let walk_edge = Edge::new(0, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
let is_valid =
validate_frontier(&walk_edge, &state, &state_model, &mfm).expect("test failed");
assert!(!is_valid);
}
#[test]
fn test_exact_sequences_partial_match_longer_sequence() {
let mut trie = SubSequenceTrie::new();
trie.insert_sequence(vec![
"walk".to_string(),
"transit".to_string(),
"bike".to_string(),
"walk".to_string(),
]);
let mode_constraint = Constraint::ExactSequences(trie);
let max_trip_legs = NonZeroU64::new(5).unwrap();
let (mam, mfm, state_model, mut state) = test_setup(
vec![mode_constraint],
"walk",
&["walk", "bike", "transit"],
max_trip_legs,
);
inject_trip_legs(
&["walk", "transit"],
&mut state,
&state_model,
&mam.mode_enumeration,
max_trip_legs,
);
let bike_edge = Edge::new(1, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
let is_valid =
validate_frontier(&bike_edge, &state, &state_model, &mfm).expect("test failed");
assert!(is_valid);
}
#[test]
fn test_multiple_constraints_all_valid() {
let max_trip_legs = NonZeroU64::new(3).unwrap();
let constraints = vec![
Constraint::AllowedModes(HashSet::from(["walk".to_string(), "bike".to_string()])),
Constraint::ModeCounts(HashMap::from([
("walk".to_string(), 2),
("bike".to_string(), 1),
])),
];
let (mam, mfm, state_model, mut state) =
test_setup(constraints, "walk", &["walk", "bike"], max_trip_legs);
inject_trip_legs(
&["walk"],
&mut state,
&state_model,
&mam.mode_enumeration,
max_trip_legs,
);
let bike_edge = Edge::new(1, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
let is_valid =
validate_frontier(&bike_edge, &state, &state_model, &mfm).expect("test failed");
assert!(is_valid);
}
#[test]
fn test_multiple_constraints_one_fails() {
let max_trip_legs = NonZeroU64::new(3).unwrap();
let mut trie = SubSequenceTrie::new();
trie.insert_sequence(vec!["walk".to_string(), "bike".to_string()]);
let constraints = vec![
Constraint::AllowedModes(HashSet::from([
"walk".to_string(), ])),
Constraint::ExactSequences(trie),
];
let (bike_mtm, bike_mfm, state_model, mut state) =
test_setup(constraints, "bike", &["walk", "bike"], max_trip_legs);
inject_trip_legs(
&["walk"],
&mut state,
&state_model,
&bike_mtm.mode_enumeration,
max_trip_legs,
);
let bike_edge = Edge::new(1, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
let is_valid =
validate_frontier(&bike_edge, &state, &state_model, &bike_mfm).expect("test failed");
assert!(!is_valid); }
#[test]
fn test_large_mode_sequence() {
let max_trip_legs = NonZeroU64::new(100).unwrap();
let mode_constraint = Constraint::ModeCounts(HashMap::from([
("walk".to_string(), 25), ("bike".to_string(), 25),
]));
let (mam, mfm, state_model, mut state) = test_setup(
vec![mode_constraint],
"walk",
&["walk", "bike"],
max_trip_legs,
);
let large_sequence: Vec<&str> = (0..50)
.map(|i| if i % 2 == 0 { "walk" } else { "bike" })
.collect();
inject_trip_legs(
&large_sequence,
&mut state,
&state_model,
&mam.mode_enumeration,
max_trip_legs,
);
let walk_edge = Edge::new(0, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
let is_valid =
validate_frontier(&walk_edge, &state, &state_model, &mfm).expect("test failed");
assert!(!is_valid); }
#[test]
fn test_max_trip_legs_would_exceed_limit() {
let max_trip_legs = NonZeroU64::new(1).unwrap();
let (bike_mtm, bike_mfm, state_model, mut state) =
test_setup(vec![], "bike", &["walk", "bike"], max_trip_legs);
inject_trip_legs(
&["walk"],
&mut state,
&state_model,
&bike_mtm.mode_enumeration,
max_trip_legs,
);
let bike_edge = Edge::new(1, 0, 0, 1, Length::new::<uom::si::length::meter>(1000.0));
let is_valid =
validate_frontier(&bike_edge, &state, &state_model, &bike_mfm).expect("test failed");
assert!(!is_valid); }
}