From 8641d6c07d364fcfa9d7defbd60c03574bc41ed2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:12:24 +0000 Subject: [PATCH 1/4] Initial plan From 3eaad851f420faf283662c77d4fc0a7bb71d719a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:21:03 +0000 Subject: [PATCH 2/4] Fix MaxTripLegs constraint bug causing infinite search expansion Co-authored-by: robfitzgerald <7003022+robfitzgerald@users.noreply.github.com> --- .../model/frontier/multimodal/constraint.rs | 2 +- .../frontier/multimodal/constraint.rs.fixed | 173 ++++++++++++++++++ .../src/model/frontier/multimodal/model.rs | 34 ++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 rust/bambam/src/model/frontier/multimodal/constraint.rs.fixed diff --git a/rust/bambam/src/model/frontier/multimodal/constraint.rs b/rust/bambam/src/model/frontier/multimodal/constraint.rs index fe6594b9..e6b93718 100644 --- a/rust/bambam/src/model/frontier/multimodal/constraint.rs +++ b/rust/bambam/src/model/frontier/multimodal/constraint.rs @@ -89,7 +89,7 @@ impl MultimodalFrontierConstraint { })?; let n_legs = match active_mode { Some(active_mode) if active_mode != edge_mode => n_existing_legs + 1, - _ => 0, + _ => n_existing_legs, }; let is_valid = n_legs <= *max_legs; Ok(is_valid) diff --git a/rust/bambam/src/model/frontier/multimodal/constraint.rs.fixed b/rust/bambam/src/model/frontier/multimodal/constraint.rs.fixed new file mode 100644 index 00000000..e6b93718 --- /dev/null +++ b/rust/bambam/src/model/frontier/multimodal/constraint.rs.fixed @@ -0,0 +1,173 @@ +use crate::model::frontier::multimodal::sequence_trie::SubSequenceTrie; +use crate::model::frontier::multimodal::{ + multimodal_frontier_ops as ops, MultimodalFrontierConstraintConfig, +}; +use crate::model::state::{ + multimodal_state_ops as state_ops, MultimodalMapping, MultimodalStateMapping, +}; +use routee_compass_core::model::{ + frontier::FrontierModelError, + network::Edge, + state::{StateModel, StateVariable}, + unit::TimeUnit, +}; +use std::collections::{HashMap, HashSet}; +use uom::si::f64::Time; + +#[derive(Debug)] +/// types of constraints to limit exponential search expansion in multimodal scenarios. +/// +/// only deals with constraints associated with multimodal metadata, since metric-based +/// constraints must be applied _after_ access + traversal metrics have been run. +pub enum MultimodalFrontierConstraint { + AllowedModes(HashSet), + ModeCounts(HashMap), + MaxTripLegs(usize), + ExactSequences(SubSequenceTrie), // MaxTime(HashMap), +} + +impl MultimodalFrontierConstraint { + /// validates an edge for traversal in a multimodal traversal + pub fn valid_frontier( + &self, + edge_mode: &str, + edge: &Edge, + state: &[StateVariable], + state_model: &StateModel, + mode_to_state: &MultimodalStateMapping, + max_trip_legs: u64, + ) -> Result { + use MultimodalFrontierConstraint as MFC; + + match self { + MFC::AllowedModes(items) => { + let result = items.contains(edge_mode); + Ok(result) + } + MFC::ModeCounts(limits) => { + let mut counts = + ops::get_mode_counts(state, state_model, max_trip_legs, mode_to_state)?; + + // simulate a mode transition if the incoming edge has a different mode than the trip's active mode + let active_mode = state_ops::get_active_leg_mode( + state, + state_model, + max_trip_legs, + mode_to_state, + ) + .map_err(|e| { + FrontierModelError::FrontierModelError(format!( + "while applying mode count frontier model constraint, {e}" + )) + })?; + if Some(edge_mode) != active_mode { + counts + .entry(edge_mode.to_string()) + .and_modify(|cnt| *cnt += 1) + .or_insert(1); + } + + Ok(ops::valid_mode_counts(&counts, limits)) + } + MFC::MaxTripLegs(max_legs) => { + // simulate a mode transition if the incoming edge has a different mode than the trip's active mode + let active_mode = state_ops::get_active_leg_mode( + state, + state_model, + max_trip_legs, + mode_to_state, + ) + .map_err(|e| { + FrontierModelError::FrontierModelError(format!( + "while applying mode count frontier model constraint, {e}" + )) + })?; + let n_existing_legs = state_ops::get_n_legs(state, state_model).map_err(|e| { + FrontierModelError::FrontierModelError( + (format!("while getting number of trip legs for this trip: {e}")), + ) + })?; + let n_legs = match active_mode { + Some(active_mode) if active_mode != edge_mode => n_existing_legs + 1, + _ => n_existing_legs, + }; + let is_valid = n_legs <= *max_legs; + Ok(is_valid) + } + MFC::ExactSequences(trie) => { + let mut modes = + state_ops::get_mode_sequence(state, state_model, max_trip_legs, mode_to_state) + .map_err(|e| { + FrontierModelError::FrontierModelError(format!( + "while testing for matching mode sub-sequence, had error: {e}" + )) + })?; + + // simulate a mode transition if the incoming edge has a different mode than the trip's active mode + let active_mode = state_ops::get_active_leg_mode( + state, + state_model, + max_trip_legs, + mode_to_state, + ) + .map_err(|e| { + FrontierModelError::FrontierModelError(format!( + "while applying mode count frontier model constraint, {e}" + )) + })?; + if Some(edge_mode) != active_mode { + modes.push(edge_mode.to_string()); + } + let is_match = trie.contains(&modes); + Ok(is_match) + } + } + } +} + +impl TryFrom<&MultimodalFrontierConstraintConfig> for MultimodalFrontierConstraint { + type Error = FrontierModelError; + + fn try_from(value: &MultimodalFrontierConstraintConfig) -> Result { + use MultimodalFrontierConstraintConfig as MFCC; + match value { + MFCC::AllowedModes { allowed_modes } => { + let modes = allowed_modes.iter().cloned().collect::>(); + Ok(Self::AllowedModes(modes)) + } + MFCC::ModeCounts { mode_counts } => { + let counts = mode_counts + .iter() + .map(|(k, v)| { + let v_usize: usize = v.get().try_into().map_err(|e| { + FrontierModelError::FrontierModelError(format!( + "while reading mode count limit: {e}" + )) + })?; + Ok((k.clone(), v_usize)) + }) + .collect::, _>>()?; + Ok(Self::ModeCounts(counts)) + } + MFCC::TripLegCount { trip_leg_count } => { + let max_usize: usize = trip_leg_count.get().try_into().map_err(|e| { + FrontierModelError::FrontierModelError(format!( + "while reading max trip leg limit: {e}" + )) + })?; + Ok(Self::MaxTripLegs(max_usize)) + } + MFCC::ExactSequences { exact_sequences } => { + let mut trie = SubSequenceTrie::new(); + for seq in exact_sequences.iter() { + trie.insert_sequence(seq.clone()); + } + Ok(Self::ExactSequences(trie)) + } + } + } +} + +// MultimodalFrontierConstraint::MaxTime(limits) => { +// ops::valid_mode_time(state, state_model, limits) +// } diff --git a/rust/bambam/src/model/frontier/multimodal/model.rs b/rust/bambam/src/model/frontier/multimodal/model.rs index e1b4aab2..e1f952e2 100644 --- a/rust/bambam/src/model/frontier/multimodal/model.rs +++ b/rust/bambam/src/model/frontier/multimodal/model.rs @@ -871,4 +871,38 @@ mod test { .expect("test failed"); assert!(!is_valid); // Should be invalid as this would create a second leg } + + #[test] + fn test_max_trip_legs_same_mode_continuation_at_limit() { + // Test that continuing with the same mode when at the limit is still invalid + // This tests the bug fix where same-mode continuation was always returning 0 legs + let max_trip_legs = 2; + let (bike_mtm, bike_mfm, state_model, mut state) = test_setup( + vec![MultimodalFrontierConstraint::MaxTripLegs(1)], + "bike", // FrontierModel for bike edges + &["walk", "bike"], + &[], + max_trip_legs, + ); + + // Set up state with 2 legs: walk then bike (exceeds limit of 1) + inject_trip_legs( + &["walk", "bike"], + &mut state, + &state_model, + &bike_mtm.mode_to_state, + max_trip_legs, + ); + + // Test continuing with bike-mode edge (same as active mode) + // edge.edge_list_id doesn't matter since we're just checking constraints, not traversal + // The important thing is that bike_mfm has mode="bike" which matches active_mode="bike" + // Before the fix, this would incorrectly return n_legs=0 and be valid + // After the fix, this should correctly use n_existing_legs=2 and be invalid + let bike_edge = Edge::new(0, 0, 0, 1, Length::new::(1000.0)); + let is_valid = bike_mfm + .valid_frontier(&bike_edge, None, &state, &state_model) + .expect("test failed"); + assert!(!is_valid); // Should be invalid as we already have 2 legs, which exceeds the limit + } } From a015ab0846fba294da8797b45c981658e44247a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:21:13 +0000 Subject: [PATCH 3/4] Remove accidentally committed backup file Co-authored-by: robfitzgerald <7003022+robfitzgerald@users.noreply.github.com> --- .../frontier/multimodal/constraint.rs.fixed | 173 ------------------ 1 file changed, 173 deletions(-) delete mode 100644 rust/bambam/src/model/frontier/multimodal/constraint.rs.fixed diff --git a/rust/bambam/src/model/frontier/multimodal/constraint.rs.fixed b/rust/bambam/src/model/frontier/multimodal/constraint.rs.fixed deleted file mode 100644 index e6b93718..00000000 --- a/rust/bambam/src/model/frontier/multimodal/constraint.rs.fixed +++ /dev/null @@ -1,173 +0,0 @@ -use crate::model::frontier::multimodal::sequence_trie::SubSequenceTrie; -use crate::model::frontier::multimodal::{ - multimodal_frontier_ops as ops, MultimodalFrontierConstraintConfig, -}; -use crate::model::state::{ - multimodal_state_ops as state_ops, MultimodalMapping, MultimodalStateMapping, -}; -use routee_compass_core::model::{ - frontier::FrontierModelError, - network::Edge, - state::{StateModel, StateVariable}, - unit::TimeUnit, -}; -use std::collections::{HashMap, HashSet}; -use uom::si::f64::Time; - -#[derive(Debug)] -/// types of constraints to limit exponential search expansion in multimodal scenarios. -/// -/// only deals with constraints associated with multimodal metadata, since metric-based -/// constraints must be applied _after_ access + traversal metrics have been run. -pub enum MultimodalFrontierConstraint { - AllowedModes(HashSet), - ModeCounts(HashMap), - MaxTripLegs(usize), - ExactSequences(SubSequenceTrie), // MaxTime(HashMap), -} - -impl MultimodalFrontierConstraint { - /// validates an edge for traversal in a multimodal traversal - pub fn valid_frontier( - &self, - edge_mode: &str, - edge: &Edge, - state: &[StateVariable], - state_model: &StateModel, - mode_to_state: &MultimodalStateMapping, - max_trip_legs: u64, - ) -> Result { - use MultimodalFrontierConstraint as MFC; - - match self { - MFC::AllowedModes(items) => { - let result = items.contains(edge_mode); - Ok(result) - } - MFC::ModeCounts(limits) => { - let mut counts = - ops::get_mode_counts(state, state_model, max_trip_legs, mode_to_state)?; - - // simulate a mode transition if the incoming edge has a different mode than the trip's active mode - let active_mode = state_ops::get_active_leg_mode( - state, - state_model, - max_trip_legs, - mode_to_state, - ) - .map_err(|e| { - FrontierModelError::FrontierModelError(format!( - "while applying mode count frontier model constraint, {e}" - )) - })?; - if Some(edge_mode) != active_mode { - counts - .entry(edge_mode.to_string()) - .and_modify(|cnt| *cnt += 1) - .or_insert(1); - } - - Ok(ops::valid_mode_counts(&counts, limits)) - } - MFC::MaxTripLegs(max_legs) => { - // simulate a mode transition if the incoming edge has a different mode than the trip's active mode - let active_mode = state_ops::get_active_leg_mode( - state, - state_model, - max_trip_legs, - mode_to_state, - ) - .map_err(|e| { - FrontierModelError::FrontierModelError(format!( - "while applying mode count frontier model constraint, {e}" - )) - })?; - let n_existing_legs = state_ops::get_n_legs(state, state_model).map_err(|e| { - FrontierModelError::FrontierModelError( - (format!("while getting number of trip legs for this trip: {e}")), - ) - })?; - let n_legs = match active_mode { - Some(active_mode) if active_mode != edge_mode => n_existing_legs + 1, - _ => n_existing_legs, - }; - let is_valid = n_legs <= *max_legs; - Ok(is_valid) - } - MFC::ExactSequences(trie) => { - let mut modes = - state_ops::get_mode_sequence(state, state_model, max_trip_legs, mode_to_state) - .map_err(|e| { - FrontierModelError::FrontierModelError(format!( - "while testing for matching mode sub-sequence, had error: {e}" - )) - })?; - - // simulate a mode transition if the incoming edge has a different mode than the trip's active mode - let active_mode = state_ops::get_active_leg_mode( - state, - state_model, - max_trip_legs, - mode_to_state, - ) - .map_err(|e| { - FrontierModelError::FrontierModelError(format!( - "while applying mode count frontier model constraint, {e}" - )) - })?; - if Some(edge_mode) != active_mode { - modes.push(edge_mode.to_string()); - } - let is_match = trie.contains(&modes); - Ok(is_match) - } - } - } -} - -impl TryFrom<&MultimodalFrontierConstraintConfig> for MultimodalFrontierConstraint { - type Error = FrontierModelError; - - fn try_from(value: &MultimodalFrontierConstraintConfig) -> Result { - use MultimodalFrontierConstraintConfig as MFCC; - match value { - MFCC::AllowedModes { allowed_modes } => { - let modes = allowed_modes.iter().cloned().collect::>(); - Ok(Self::AllowedModes(modes)) - } - MFCC::ModeCounts { mode_counts } => { - let counts = mode_counts - .iter() - .map(|(k, v)| { - let v_usize: usize = v.get().try_into().map_err(|e| { - FrontierModelError::FrontierModelError(format!( - "while reading mode count limit: {e}" - )) - })?; - Ok((k.clone(), v_usize)) - }) - .collect::, _>>()?; - Ok(Self::ModeCounts(counts)) - } - MFCC::TripLegCount { trip_leg_count } => { - let max_usize: usize = trip_leg_count.get().try_into().map_err(|e| { - FrontierModelError::FrontierModelError(format!( - "while reading max trip leg limit: {e}" - )) - })?; - Ok(Self::MaxTripLegs(max_usize)) - } - MFCC::ExactSequences { exact_sequences } => { - let mut trie = SubSequenceTrie::new(); - for seq in exact_sequences.iter() { - trie.insert_sequence(seq.clone()); - } - Ok(Self::ExactSequences(trie)) - } - } - } -} - -// MultimodalFrontierConstraint::MaxTime(limits) => { -// ops::valid_mode_time(state, state_model, limits) -// } From 63f708e09a39c9885475b7eff0b5348f791361e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:22:21 +0000 Subject: [PATCH 4/4] Clarify test comments to distinguish buffer size from constraint limit Co-authored-by: robfitzgerald <7003022+robfitzgerald@users.noreply.github.com> --- rust/bambam/src/model/frontier/multimodal/model.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rust/bambam/src/model/frontier/multimodal/model.rs b/rust/bambam/src/model/frontier/multimodal/model.rs index e1f952e2..eddf1f1f 100644 --- a/rust/bambam/src/model/frontier/multimodal/model.rs +++ b/rust/bambam/src/model/frontier/multimodal/model.rs @@ -876,16 +876,20 @@ mod test { fn test_max_trip_legs_same_mode_continuation_at_limit() { // Test that continuing with the same mode when at the limit is still invalid // This tests the bug fix where same-mode continuation was always returning 0 legs - let max_trip_legs = 2; + + // max_trip_legs is the state buffer size, constraint is the actual limit + let max_trip_legs = 2; // State buffer can hold 2 legs + let constraint_limit = 1; // But we only allow 1 leg + let (bike_mtm, bike_mfm, state_model, mut state) = test_setup( - vec![MultimodalFrontierConstraint::MaxTripLegs(1)], + vec![MultimodalFrontierConstraint::MaxTripLegs(constraint_limit)], "bike", // FrontierModel for bike edges &["walk", "bike"], &[], max_trip_legs, ); - // Set up state with 2 legs: walk then bike (exceeds limit of 1) + // Set up state with 2 legs: walk then bike (exceeds constraint_limit of 1) inject_trip_legs( &["walk", "bike"], &mut state, @@ -903,6 +907,6 @@ mod test { let is_valid = bike_mfm .valid_frontier(&bike_edge, None, &state, &state_model) .expect("test failed"); - assert!(!is_valid); // Should be invalid as we already have 2 legs, which exceeds the limit + assert!(!is_valid); // Should be invalid as we already have 2 legs, which exceeds constraint_limit of 1 } }