use crate::errors::ProjectionError;
use crate::models::{AssociatedNetElement, Netelement};
use crate::path::candidate::CandidateNetElement;
use crate::path::graph::{
cached_shortest_path_distance, shortest_path_route, NetelementSide, ShortestPathCache,
};
use crate::path::probability::{calculate_transition_probability, is_near_netelement_edge};
use crate::path::PathConfig;
use geo::{HaversineDistance, Point};
use petgraph::graph::{DiGraph, NodeIndex};
use petgraph::visit::EdgeRef;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ViterbiResult {
pub subsequences: Vec<ViterbiSubsequence>,
pub transition_records: Vec<(usize, usize, usize, usize, f64)>,
}
#[derive(Debug, Clone)]
pub struct ViterbiSubsequence {
pub states: Vec<(usize, usize)>,
pub log_probability: f64,
}
#[allow(clippy::too_many_arguments)]
pub fn viterbi_decode(
position_candidates: &[Vec<CandidateNetElement>],
position_probabilities: &[Vec<f64>],
netelements: &[Netelement],
netelement_index: &HashMap<String, usize>,
graph: &DiGraph<NetelementSide, f64>,
node_map: &HashMap<NetelementSide, NodeIndex>,
cache: &mut ShortestPathCache,
config: &PathConfig,
) -> Result<ViterbiResult, ProjectionError> {
let t_count = position_candidates.len();
if t_count == 0 {
return Ok(ViterbiResult {
subsequences: vec![],
transition_records: vec![],
});
}
let near_edge: Vec<Vec<bool>> = position_candidates
.iter()
.map(|cands| {
cands
.iter()
.map(|c| {
if let Some(&ne_idx) = netelement_index.get(&c.netelement_id) {
is_near_netelement_edge(
&c.projected_point,
&netelements[ne_idx].geometry,
config.edge_zone_distance,
)
} else {
true }
})
.collect()
})
.collect();
let mut log_v: Vec<Vec<f64>> = Vec::with_capacity(t_count);
let mut backptr: Vec<Vec<Option<usize>>> = Vec::with_capacity(t_count);
let mut backptr_time: Vec<Vec<Option<usize>>> = Vec::with_capacity(t_count);
let mut transition_records: Vec<(usize, usize, usize, usize, f64)> = Vec::new();
{
let cands = &position_candidates[0];
let probs = &position_probabilities[0];
let mut lv = Vec::with_capacity(cands.len());
let mut bp = Vec::with_capacity(cands.len());
for (j, _) in cands.iter().enumerate() {
let emission = probs.get(j).copied().unwrap_or(0.0);
lv.push(safe_ln(emission));
bp.push(None);
}
log_v.push(lv);
backptr.push(bp);
backptr_time.push(vec![None; position_candidates[0].len()]);
}
const NO_TRANSITION_PENALTY: f64 = -23.0;
const EMISSION_FLOOR: f64 = 1e-10;
for t in 1..t_count {
let curr_cands = &position_candidates[t];
let curr_probs = &position_probabilities[t];
if curr_cands.is_empty() {
log_v.push(vec![]);
backptr.push(vec![]);
backptr_time.push(vec![]);
continue;
}
let mut prev_t = None;
for pt in (0..t).rev() {
if !log_v[pt].is_empty() && log_v[pt].iter().any(|&v| v != f64::NEG_INFINITY) {
prev_t = Some(pt);
break;
}
}
if prev_t.is_none() {
let mut lv = Vec::with_capacity(curr_cands.len());
let mut bp = Vec::with_capacity(curr_cands.len());
for (j, _) in curr_cands.iter().enumerate() {
let emission = curr_probs.get(j).copied().unwrap_or(0.0);
lv.push(safe_ln(emission));
bp.push(None);
}
log_v.push(lv);
backptr.push(bp);
backptr_time.push(vec![None; curr_cands.len()]);
continue;
}
let prev_t = prev_t.unwrap();
let prev_cands = &position_candidates[prev_t];
let prev_lv = &log_v[prev_t];
let mut lv = vec![f64::NEG_INFINITY; curr_cands.len()];
let mut bp: Vec<Option<(usize, usize)>> = vec![None; curr_cands.len()];
for (j, cand_j) in curr_cands.iter().enumerate() {
let emission_j = curr_probs.get(j).copied().unwrap_or(0.0);
let ln_emission_j = safe_ln(emission_j.max(EMISSION_FLOOR));
for (i, cand_i) in prev_cands.iter().enumerate() {
if prev_lv[i] == f64::NEG_INFINITY {
continue;
}
let ln_trans = compute_log_transition(
cand_i,
cand_j,
i,
j,
prev_t,
t,
&near_edge,
netelements,
netelement_index,
graph,
node_map,
cache,
config,
);
if ln_trans == f64::NEG_INFINITY {
continue;
}
transition_records.push((prev_t, i, t, j, ln_trans.exp()));
let score = prev_lv[i] + ln_trans + ln_emission_j;
if score > lv[j] {
lv[j] = score;
bp[j] = Some((prev_t, i));
}
}
}
if lv.iter().all(|&v| v == f64::NEG_INFINITY) {
let best_prev_i = prev_lv
.iter()
.enumerate()
.filter(|(_, &v)| v != f64::NEG_INFINITY)
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.map(|(i, _)| i);
if let Some(best_i) = best_prev_i {
let carry_score = prev_lv[best_i] + NO_TRANSITION_PENALTY;
for (j, _) in curr_cands.iter().enumerate() {
let emission = curr_probs.get(j).copied().unwrap_or(0.0);
let ln_em = safe_ln(emission.max(EMISSION_FLOOR));
lv[j] = carry_score + ln_em;
bp[j] = Some((prev_t, best_i));
}
}
}
log_v.push(lv);
let flat_bp: Vec<Option<usize>> = bp.iter().map(|opt| opt.map(|(_, i)| i)).collect();
let time_bp: Vec<Option<usize>> = bp.iter().map(|opt| opt.map(|(pt, _)| pt)).collect();
backptr.push(flat_bp);
backptr_time.push(time_bp);
}
let subsequences = backtrace_continuous(&log_v, &backptr, &backptr_time, t_count);
Ok(ViterbiResult {
subsequences,
transition_records,
})
}
#[allow(clippy::too_many_arguments)]
fn compute_log_transition(
cand_i: &CandidateNetElement,
cand_j: &CandidateNetElement,
i: usize,
j: usize,
t_prev: usize,
t_curr: usize,
near_edge: &[Vec<bool>],
netelements: &[Netelement],
netelement_index: &HashMap<String, usize>,
graph: &DiGraph<NetelementSide, f64>,
node_map: &HashMap<NetelementSide, NodeIndex>,
cache: &mut ShortestPathCache,
config: &PathConfig,
) -> f64 {
let same_ne = cand_i.netelement_id == cand_j.netelement_id;
if same_ne {
return 0.0; }
let i_near = near_edge[t_prev][i];
let j_near = near_edge[t_curr][j];
if !i_near && !j_near {
return f64::NEG_INFINITY; }
let Some(&ne_i_idx) = netelement_index.get(&cand_i.netelement_id) else {
return f64::NEG_INFINITY;
};
let Some(&ne_j_idx) = netelement_index.get(&cand_j.netelement_id) else {
return f64::NEG_INFINITY;
};
let from_sides = candidate_sides(&cand_i.netelement_id);
let to_sides = candidate_sides(&cand_j.netelement_id);
let gc_distance = cand_i
.projected_point
.haversine_distance(&cand_j.projected_point);
let ne_i_geom = &netelements[ne_i_idx].geometry;
let ne_j_geom = &netelements[ne_j_idx].geometry;
let mut best_ln_trans = f64::NEG_INFINITY;
for from_side in &from_sides {
for to_side in &to_sides {
if let Some(d) =
cached_shortest_path_distance(cache, graph, node_map, from_side, to_side)
{
let from_partial =
partial_netelement_distance(cand_i, from_side.position, &netelements[ne_i_idx]);
let to_partial =
partial_netelement_distance(cand_j, to_side.position, &netelements[ne_j_idx]);
let route_distance = from_partial + d + to_partial;
let base_trans =
calculate_transition_probability(route_distance, gc_distance, config.beta);
let turn_factor = netelement_connection_turn_factor(
ne_i_geom,
from_side.position,
ne_j_geom,
to_side.position,
config.turn_scale,
);
let combined = base_trans * turn_factor;
let ln_combined = safe_ln(combined);
if ln_combined > best_ln_trans {
best_ln_trans = ln_combined;
}
}
}
}
best_ln_trans
}
fn netelement_connection_turn_factor(
from_geom: &geo::LineString<f64>,
from_side: u8,
to_geom: &geo::LineString<f64>,
to_side: u8,
turn_scale: f64,
) -> f64 {
use crate::path::candidate::{directional_heading_difference, haversine_bearing};
let from_pts: Vec<Point<f64>> = from_geom.points().collect();
let to_pts: Vec<Point<f64>> = to_geom.points().collect();
if from_pts.len() < 2 || to_pts.len() < 2 {
return 1.0; }
let exit_heading = if from_side == 0 {
haversine_bearing(&from_pts[1], &from_pts[0])
} else {
let n = from_pts.len();
haversine_bearing(&from_pts[n - 2], &from_pts[n - 1])
};
let entry_heading = if to_side == 0 {
haversine_bearing(&to_pts[0], &to_pts[1])
} else {
let n = to_pts.len();
haversine_bearing(&to_pts[n - 1], &to_pts[n - 2])
};
let turn_angle = directional_heading_difference(exit_heading, entry_heading);
(-turn_angle / turn_scale).exp()
}
fn candidate_sides(netelement_id: &str) -> [NetelementSide; 2] {
[
NetelementSide {
netelement_id: netelement_id.to_string(),
position: 0,
},
NetelementSide {
netelement_id: netelement_id.to_string(),
position: 1,
},
]
}
fn partial_netelement_distance(
cand: &CandidateNetElement,
side: u8,
netelement: &Netelement,
) -> f64 {
use geo::HaversineLength;
let length = netelement.geometry.haversine_length();
if side == 0 {
cand.intrinsic_coordinate * length
} else {
(1.0 - cand.intrinsic_coordinate) * length
}
}
fn safe_ln(x: f64) -> f64 {
if x <= 0.0 {
f64::NEG_INFINITY
} else {
x.ln()
}
}
#[allow(dead_code)]
fn backtrace(
log_v: &[Vec<f64>],
backptr: &[Vec<Option<usize>>],
subseq_starts: &[usize],
t_count: usize,
) -> Vec<ViterbiSubsequence> {
let mut result: Vec<ViterbiSubsequence> = Vec::new();
for (seg_idx, &start_t) in subseq_starts.iter().enumerate() {
let end_t = if seg_idx + 1 < subseq_starts.len() {
subseq_starts[seg_idx + 1]
} else {
t_count
};
let mut last_valid_t = None;
for t in (start_t..end_t).rev() {
if !log_v[t].is_empty() && log_v[t].iter().any(|&v| v != f64::NEG_INFINITY) {
last_valid_t = Some(t);
break;
}
}
let Some(term_t) = last_valid_t else {
continue; };
let (best_j, best_log) = log_v[term_t]
.iter()
.enumerate()
.filter(|(_, &v)| v != f64::NEG_INFINITY)
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap();
let mut states = Vec::with_capacity(term_t - start_t + 1);
let mut j = best_j;
states.push((term_t, j));
let mut t = term_t;
while t > start_t {
if let Some(prev_j) = backptr[t][j] {
t -= 1;
j = prev_j;
states.push((t, j));
} else {
break;
}
}
states.reverse();
result.push(ViterbiSubsequence {
states,
log_probability: *best_log,
});
}
result
}
fn backtrace_continuous(
log_v: &[Vec<f64>],
backptr: &[Vec<Option<usize>>],
backptr_time: &[Vec<Option<usize>>],
t_count: usize,
) -> Vec<ViterbiSubsequence> {
let mut last_valid_t = None;
for t in (0..t_count).rev() {
if !log_v[t].is_empty() && log_v[t].iter().any(|&v| v != f64::NEG_INFINITY) {
last_valid_t = Some(t);
break;
}
}
let Some(term_t) = last_valid_t else {
return vec![]; };
let (best_j, best_log) = log_v[term_t]
.iter()
.enumerate()
.filter(|(_, &v)| v != f64::NEG_INFINITY)
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap();
let mut states = Vec::with_capacity(term_t + 1);
let mut j = best_j;
let mut t = term_t;
states.push((t, j));
loop {
let prev_j = backptr[t][j];
let prev_t = backptr_time[t][j];
match (prev_t, prev_j) {
(Some(pt), Some(pj)) => {
t = pt;
j = pj;
states.push((t, j));
}
_ => break,
}
}
states.reverse();
vec![ViterbiSubsequence {
states,
log_probability: *best_log,
}]
}
pub fn build_path_from_viterbi(
viterbi: &ViterbiResult,
position_candidates: &[Vec<CandidateNetElement>],
netelements: &[Netelement],
netelement_index: &HashMap<String, usize>,
graph: &DiGraph<NetelementSide, f64>,
node_map: &HashMap<NetelementSide, NodeIndex>,
cache: &mut ShortestPathCache,
) -> Result<Vec<AssociatedNetElement>, ProjectionError> {
let mut segments: Vec<AssociatedNetElement> = Vec::new();
for subseq in &viterbi.subsequences {
if subseq.states.is_empty() {
continue;
}
let mut groups: Vec<NetelementGroup> = Vec::new();
for &(pos_idx, cand_idx) in &subseq.states {
let cand = &position_candidates[pos_idx][cand_idx];
if let Some(last) = groups.last_mut() {
if last.netelement_id == cand.netelement_id {
last.update(pos_idx, cand);
continue;
}
}
groups.push(NetelementGroup::new(pos_idx, cand));
}
for (g_idx, group) in groups.iter().enumerate() {
if g_idx > 0 {
let prev = &groups[g_idx - 1];
insert_bridges(
prev,
group,
&mut segments,
netelements,
netelement_index,
graph,
node_map,
cache,
)?;
}
segments.push(group.to_associated_net_element()?);
}
}
Ok(segments)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SanityDecision {
pub pair_index: usize,
pub from_netelement_id: String,
pub to_netelement_id: String,
pub reachable: bool,
pub action: String,
pub rerouted_via: Vec<String>,
pub warning: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GapFill {
pub pair_index: usize,
pub from_netelement_id: String,
pub to_netelement_id: String,
pub route_found: bool,
pub inserted_netelements: Vec<String>,
pub warning: String,
}
#[allow(clippy::too_many_arguments)]
pub fn validate_path_navigability(
segments: Vec<AssociatedNetElement>,
_netelements: &[Netelement],
netelement_index: &HashMap<String, usize>,
graph: &DiGraph<NetelementSide, f64>,
node_map: &HashMap<NetelementSide, NodeIndex>,
cache: &mut ShortestPathCache,
) -> (Vec<AssociatedNetElement>, Vec<String>, Vec<SanityDecision>) {
if segments.len() < 2 {
return (segments, vec![], vec![]);
}
let mut validated: Vec<AssociatedNetElement> = Vec::new();
let mut warnings: Vec<String> = Vec::new();
let mut decisions: Vec<SanityDecision> = Vec::new();
let mut pair_index: usize = 0;
validated.push(segments[0].clone());
let mut i = 1;
while i < segments.len() {
let last_ne_id = validated.last().unwrap().netelement_id.clone();
let last_gnss_end = validated.last().unwrap().gnss_end_index;
let candidate = &segments[i];
if last_ne_id == candidate.netelement_id {
decisions.push(SanityDecision {
pair_index,
from_netelement_id: last_ne_id,
to_netelement_id: candidate.netelement_id.clone(),
reachable: true,
action: "kept".to_string(),
rerouted_via: vec![],
warning: String::new(),
});
validated.push(candidate.clone());
pair_index += 1;
i += 1;
continue;
}
let from_sides = candidate_sides(&last_ne_id);
let to_sides = candidate_sides(&candidate.netelement_id);
let mut reachable = false;
for from in &from_sides {
for to in &to_sides {
if cached_shortest_path_distance(cache, graph, node_map, from, to).is_some() {
reachable = true;
break;
}
}
if reachable {
break;
}
}
if reachable {
decisions.push(SanityDecision {
pair_index,
from_netelement_id: last_ne_id,
to_netelement_id: candidate.netelement_id.clone(),
reachable: true,
action: "kept".to_string(),
rerouted_via: vec![],
warning: String::new(),
});
validated.push(candidate.clone());
pair_index += 1;
i += 1;
continue;
}
let warn_msg = format!(
"Removed unreachable segment {} (no navigable path from {})",
candidate.netelement_id, last_ne_id
);
warnings.push(warn_msg.clone());
let mut rerouted = false;
let mut rerouted_via: Vec<String> = Vec::new();
if i + 1 < segments.len() {
let next = &segments[i + 1];
let fwd_from_sides = candidate_sides(&last_ne_id);
let fwd_to_sides = candidate_sides(&next.netelement_id);
let mut best_route_cost: Option<f64> = None;
let mut best_from_side = 0u8;
let mut best_to_side = 0u8;
for from in &fwd_from_sides {
for to in &fwd_to_sides {
if let Some(d) = cached_shortest_path_distance(cache, graph, node_map, from, to)
{
if best_route_cost.is_none() || d < best_route_cost.unwrap() {
best_route_cost = Some(d);
best_from_side = from.position;
best_to_side = to.position;
}
}
}
}
if let Some(cost) = best_route_cost {
rerouted = true;
if cost > 1e-9 {
let from_side = NetelementSide {
netelement_id: last_ne_id.clone(),
position: best_from_side,
};
let to_side = NetelementSide {
netelement_id: next.netelement_id.clone(),
position: best_to_side,
};
let bridge_ne_ids = trace_intermediate_netelements(
graph,
node_map,
&from_side,
&to_side,
&last_ne_id,
&next.netelement_id,
);
let gnss_idx = last_gnss_end;
for ne_id in &bridge_ne_ids {
if netelement_index.contains_key(ne_id) {
if let Ok(bridge) = AssociatedNetElement::new(
ne_id.clone(),
1.0,
0.0,
1.0,
gnss_idx,
gnss_idx,
) {
validated.push(bridge);
}
}
}
rerouted_via = bridge_ne_ids;
}
}
}
let action = if rerouted { "rerouted" } else { "removed" };
decisions.push(SanityDecision {
pair_index,
from_netelement_id: last_ne_id,
to_netelement_id: candidate.netelement_id.clone(),
reachable: false,
action: action.to_string(),
rerouted_via,
warning: warn_msg,
});
pair_index += 1;
i += 1;
}
let validated = remove_oscillations(validated, &mut warnings, &mut decisions, &mut pair_index);
let validated = remove_direction_violations(
validated,
graph,
node_map,
&mut warnings,
&mut decisions,
&mut pair_index,
);
(validated, warnings, decisions)
}
fn remove_oscillations(
segments: Vec<AssociatedNetElement>,
warnings: &mut Vec<String>,
decisions: &mut Vec<SanityDecision>,
pair_index: &mut usize,
) -> Vec<AssociatedNetElement> {
if segments.len() < 3 {
return segments;
}
let mut result = segments;
let mut changed = true;
while changed {
changed = false;
let mut i = 0;
while i < result.len() {
let ne_id = result[i].netelement_id.clone();
let mut j_opt: Option<usize> = None;
for (k, step) in result.iter().enumerate().skip(i + 1) {
if step.netelement_id == ne_id {
j_opt = Some(k);
break;
}
}
let j = match j_opt {
Some(v) => v,
None => {
i += 1;
continue;
}
};
let first_gnss_count = result[i]
.gnss_end_index
.saturating_sub(result[i].gnss_start_index);
let intermediate_gnss_count = if j > i + 1 {
result[j - 1]
.gnss_end_index
.saturating_sub(result[i + 1].gnss_start_index)
} else {
0
};
let intermediate_ne_count = j - i - 1;
let is_oscillation = intermediate_ne_count <= MAX_OSCILLATION_INTERMEDIATE_NES
&& (intermediate_gnss_count <= first_gnss_count || intermediate_gnss_count < 10);
if !is_oscillation {
i += 1;
continue;
}
let removed_ne_ids: Vec<String> = result[(i + 1)..j]
.iter()
.map(|s| s.netelement_id.clone())
.collect();
let warn_msg = format!(
"Collapsed oscillation: {} revisited after [{}] (intermediate covered {} GNSS positions)",
ne_id,
removed_ne_ids.join(", "),
intermediate_gnss_count,
);
warnings.push(warn_msg.clone());
decisions.push(SanityDecision {
pair_index: *pair_index,
from_netelement_id: ne_id.clone(),
to_netelement_id: ne_id.clone(),
reachable: true,
action: "collapsed-oscillation".to_string(),
rerouted_via: removed_ne_ids,
warning: warn_msg,
});
*pair_index += 1;
result[i].gnss_end_index = result[j].gnss_end_index;
if result[j].gnss_start_index != result[j].gnss_end_index {
result[i].end_intrinsic = result[j].end_intrinsic;
}
result.drain((i + 1)..=j);
changed = true;
}
}
result
}
fn has_direct_connection(
graph: &DiGraph<NetelementSide, f64>,
node_map: &HashMap<NetelementSide, NodeIndex>,
from: &NetelementSide,
to: &NetelementSide,
) -> bool {
let Some(&from_idx) = node_map.get(from) else {
return false;
};
let Some(&to_idx) = node_map.get(to) else {
return false;
};
graph
.edges_directed(from_idx, petgraph::Direction::Outgoing)
.any(|e| e.target() == to_idx && *e.weight() < 1e-9)
}
fn triple_is_consistent(
ne_a: &str,
ne_b: &str,
ne_c: &str,
graph: &DiGraph<NetelementSide, f64>,
node_map: &HashMap<NetelementSide, NodeIndex>,
) -> bool {
if ne_a == ne_b || ne_b == ne_c {
return true;
}
for a_exit in [0u8, 1] {
let a_side = NetelementSide {
netelement_id: ne_a.to_string(),
position: a_exit,
};
for b_entry in [0u8, 1] {
let b_entry_side = NetelementSide {
netelement_id: ne_b.to_string(),
position: b_entry,
};
if !has_direct_connection(graph, node_map, &a_side, &b_entry_side) {
continue;
}
let b_exit_side = NetelementSide {
netelement_id: ne_b.to_string(),
position: 1 - b_entry,
};
for c_entry in [0u8, 1] {
let c_entry_side = NetelementSide {
netelement_id: ne_c.to_string(),
position: c_entry,
};
if has_direct_connection(graph, node_map, &b_exit_side, &c_entry_side) {
return true;
}
}
}
}
false
}
const DIRECTION_REMOVAL_GNSS_THRESHOLD: usize = 100;
const MAX_OSCILLATION_INTERMEDIATE_NES: usize = 3;
const MAX_DIRECTION_CASCADE_REMOVALS: usize = 3;
fn remove_direction_violations(
segments: Vec<AssociatedNetElement>,
graph: &DiGraph<NetelementSide, f64>,
node_map: &HashMap<NetelementSide, NodeIndex>,
warnings: &mut Vec<String>,
decisions: &mut Vec<SanityDecision>,
pair_index: &mut usize,
) -> Vec<AssociatedNetElement> {
if segments.len() < 3 {
return segments;
}
let gnss_span = |seg: &AssociatedNetElement| -> usize {
seg.gnss_end_index.saturating_sub(seg.gnss_start_index)
};
let is_bridge =
|seg: &AssociatedNetElement| -> bool { seg.gnss_start_index == seg.gnss_end_index };
let is_removable = |seg: &AssociatedNetElement| -> bool {
is_bridge(seg) || gnss_span(seg) < DIRECTION_REMOVAL_GNSS_THRESHOLD
};
let mut result = segments;
let mut changed = true;
let mut warned_triples: std::collections::HashSet<(String, String, String)> =
std::collections::HashSet::new();
let mut cascade_as_anchor: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
let mut cascade_as_protected: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
while changed {
changed = false;
let mut i = 1;
while i < result.len().saturating_sub(1) {
let ne_a = result[i - 1].netelement_id.clone();
let ne_b = result[i].netelement_id.clone();
let ne_c = result[i + 1].netelement_id.clone();
if triple_is_consistent(&ne_a, &ne_b, &ne_c, graph, node_map) {
i += 1;
continue;
}
let b_anchor = cascade_as_anchor.get(&ne_b).copied().unwrap_or(0);
let b_protected = cascade_as_protected.get(&ne_b).copied().unwrap_or(0);
let c_would_be_removable = is_removable(&result[i + 1]);
if (b_anchor >= MAX_DIRECTION_CASCADE_REMOVALS
|| b_protected >= MAX_DIRECTION_CASCADE_REMOVALS)
&& c_would_be_removable
{
if !is_bridge(&result[i]) && i > 0 {
let end = result[i].gnss_end_index;
if result[i - 1].gnss_end_index < end {
result[i - 1].gnss_end_index = end;
}
}
let warn = format!(
"Removed {} (cascade: anchor={} protected={} at triple {}/{}/{})",
ne_b, b_anchor, b_protected, ne_a, ne_b, ne_c,
);
warnings.push(warn.clone());
decisions.push(SanityDecision {
pair_index: *pair_index,
from_netelement_id: ne_a,
to_netelement_id: ne_c,
reachable: false,
action: "removed-direction-cascade".to_string(),
rerouted_via: vec![ne_b],
warning: warn,
});
*pair_index += 1;
result.remove(i);
changed = true;
break;
}
if ne_a == ne_c {
let target_idx = i + 1;
let target_ne = result[target_idx].netelement_id.clone();
if !is_bridge(&result[target_idx]) && target_idx > 0 {
let end = result[target_idx].gnss_end_index;
if result[target_idx - 1].gnss_end_index < end {
result[target_idx - 1].gnss_end_index = end;
}
}
let warn = format!(
"Removed {} (oscillation remnant: triple {}/{}/{})",
target_ne, ne_a, ne_b, ne_c,
);
warnings.push(warn.clone());
decisions.push(SanityDecision {
pair_index: *pair_index,
from_netelement_id: ne_a,
to_netelement_id: ne_c,
reachable: false,
action: "removed-direction-violation".to_string(),
rerouted_via: vec![target_ne],
warning: warn,
});
*pair_index += 1;
result.remove(target_idx);
changed = true;
break;
}
let a_to_b_connected = (0u8..=1).any(|ap| {
(0u8..=1).any(|bp| {
has_direct_connection(
graph,
node_map,
&NetelementSide {
netelement_id: ne_a.clone(),
position: ap,
},
&NetelementSide {
netelement_id: ne_b.clone(),
position: bp,
},
)
})
});
let primary_idx = if a_to_b_connected {
i } else {
match (is_bridge(&result[i - 1]), is_bridge(&result[i])) {
(_, true) => i,
(true, false) => i - 1,
_ => {
if gnss_span(&result[i - 1]) <= gnss_span(&result[i]) {
i - 1
} else {
i
}
}
}
};
let target_idx = if is_removable(&result[primary_idx]) {
primary_idx
} else if a_to_b_connected {
if is_removable(&result[i + 1]) {
i + 1
} else {
let triple_key = (ne_a.clone(), ne_b.clone(), ne_c.clone());
if warned_triples.insert(triple_key) {
let warn = format!(
"Directional violation at {}/{}/{} \
(kept: segments too significant to remove automatically)",
ne_a, ne_b, ne_c,
);
warnings.push(warn.clone());
decisions.push(SanityDecision {
pair_index: *pair_index,
from_netelement_id: ne_a,
to_netelement_id: ne_c,
reachable: false,
action: "kept-direction-warning".to_string(),
rerouted_via: vec![ne_b],
warning: warn,
});
*pair_index += 1;
}
i += 1;
continue;
}
} else {
let triple_key = (ne_a.clone(), ne_b.clone(), ne_c.clone());
if warned_triples.insert(triple_key) {
let warn = format!(
"Directional violation at {}/{}/{} \
(kept: segments too significant to remove automatically)",
ne_a, ne_b, ne_c,
);
warnings.push(warn.clone());
decisions.push(SanityDecision {
pair_index: *pair_index,
from_netelement_id: ne_a,
to_netelement_id: ne_c,
reachable: false,
action: "kept-direction-warning".to_string(),
rerouted_via: vec![ne_b],
warning: warn,
});
*pair_index += 1;
}
i += 1;
continue;
};
let target_ne = result[target_idx].netelement_id.clone();
if !is_bridge(&result[target_idx]) {
if target_idx > 0 {
let end = result[target_idx].gnss_end_index;
if result[target_idx - 1].gnss_end_index < end {
result[target_idx - 1].gnss_end_index = end;
}
} else if target_idx + 1 < result.len() {
let start = result[target_idx].gnss_start_index;
if result[target_idx + 1].gnss_start_index > start {
result[target_idx + 1].gnss_start_index = start;
}
}
}
if target_idx == i {
*cascade_as_anchor.entry(ne_a.clone()).or_insert(0) += 1;
} else if target_idx == i + 1 {
*cascade_as_protected.entry(ne_b.clone()).or_insert(0) += 1;
}
let warn = format!(
"Removed {} (directional violation: triple {}/{}/{})",
target_ne, ne_a, ne_b, ne_c,
);
warnings.push(warn.clone());
decisions.push(SanityDecision {
pair_index: *pair_index,
from_netelement_id: ne_a,
to_netelement_id: ne_c,
reachable: false,
action: "removed-direction-violation".to_string(),
rerouted_via: vec![target_ne],
warning: warn,
});
*pair_index += 1;
result.remove(target_idx);
changed = true;
break;
}
}
result
}
struct NetelementGroup {
netelement_id: String,
min_intrinsic: f64,
max_intrinsic: f64,
first_pos_idx: usize,
last_pos_idx: usize,
count: usize,
}
impl NetelementGroup {
fn new(pos_idx: usize, cand: &CandidateNetElement) -> Self {
Self {
netelement_id: cand.netelement_id.clone(),
min_intrinsic: cand.intrinsic_coordinate,
max_intrinsic: cand.intrinsic_coordinate,
first_pos_idx: pos_idx,
last_pos_idx: pos_idx,
count: 1,
}
}
fn update(&mut self, pos_idx: usize, cand: &CandidateNetElement) {
self.min_intrinsic = self.min_intrinsic.min(cand.intrinsic_coordinate);
self.max_intrinsic = self.max_intrinsic.max(cand.intrinsic_coordinate);
self.last_pos_idx = pos_idx;
self.count += 1;
}
fn to_associated_net_element(&self) -> Result<AssociatedNetElement, ProjectionError> {
AssociatedNetElement::new(
self.netelement_id.clone(),
1.0, self.min_intrinsic,
self.max_intrinsic,
self.first_pos_idx,
self.last_pos_idx,
)
}
}
#[allow(clippy::too_many_arguments)]
fn insert_bridges(
prev: &NetelementGroup,
next: &NetelementGroup,
segments: &mut Vec<AssociatedNetElement>,
_netelements: &[Netelement],
netelement_index: &HashMap<String, usize>,
graph: &DiGraph<NetelementSide, f64>,
node_map: &HashMap<NetelementSide, NodeIndex>,
cache: &mut ShortestPathCache,
) -> Result<(), ProjectionError> {
let from_sides = candidate_sides(&prev.netelement_id);
let to_sides = candidate_sides(&next.netelement_id);
let mut best_route: Option<f64> = None;
let mut best_from_side = 0u8;
let mut best_to_side = 0u8;
for from in &from_sides {
for to in &to_sides {
if let Some(d) = cached_shortest_path_distance(cache, graph, node_map, from, to) {
if best_route.is_none() || d < best_route.unwrap() {
best_route = Some(d);
best_from_side = from.position;
best_to_side = to.position;
}
}
}
}
if best_route.is_none() {
return Ok(());
}
let route_cost = best_route.unwrap();
if route_cost < 1e-9 {
return Ok(());
}
let from_side = NetelementSide {
netelement_id: prev.netelement_id.clone(),
position: best_from_side,
};
let to_side = NetelementSide {
netelement_id: next.netelement_id.clone(),
position: best_to_side,
};
let bridge_ne_ids = trace_intermediate_netelements(
graph,
node_map,
&from_side,
&to_side,
&prev.netelement_id,
&next.netelement_id,
);
let gnss_idx = prev.last_pos_idx; for ne_id in &bridge_ne_ids {
if netelement_index.contains_key(ne_id) {
segments.push(AssociatedNetElement::new(
ne_id.clone(),
1.0, 0.0, 1.0,
gnss_idx,
gnss_idx,
)?);
}
}
Ok(())
}
pub fn fill_path_gaps(
segments: Vec<AssociatedNetElement>,
netelement_index: &HashMap<String, usize>,
graph: &DiGraph<NetelementSide, f64>,
node_map: &HashMap<NetelementSide, NodeIndex>,
cache: &mut ShortestPathCache,
) -> (Vec<AssociatedNetElement>, Vec<String>, Vec<GapFill>) {
if segments.len() < 2 {
return (segments, vec![], vec![]);
}
let mut result: Vec<AssociatedNetElement> = Vec::new();
let mut warnings: Vec<String> = Vec::new();
let mut gap_fills: Vec<GapFill> = Vec::new();
result.push(segments[0].clone());
let mut cursor = 1usize;
let mut effective_prev = 0usize;
while cursor < segments.len() {
let prev = &segments[effective_prev];
let next = &segments[cursor];
if prev.netelement_id == next.netelement_id {
result.push(next.clone());
effective_prev = cursor;
cursor += 1;
continue;
}
let from_sides = candidate_sides(&prev.netelement_id);
let to_sides = candidate_sides(&next.netelement_id);
let mut directly_connected = false;
for from in &from_sides {
for to in &to_sides {
if has_direct_connection(graph, node_map, from, to) {
directly_connected = true;
break;
}
}
if directly_connected {
break;
}
}
if directly_connected {
result.push(next.clone());
effective_prev = cursor;
cursor += 1;
continue;
}
let mut best_route_cost: Option<f64> = None;
let mut best_from_side = 0u8;
let mut best_to_side = 0u8;
for from in &from_sides {
for to in &to_sides {
if let Some(d) = cached_shortest_path_distance(cache, graph, node_map, from, to) {
if best_route_cost.is_none() || d < best_route_cost.unwrap() {
best_route_cost = Some(d);
best_from_side = from.position;
best_to_side = to.position;
}
}
}
}
if let Some(_cost) = best_route_cost {
let from_side = NetelementSide {
netelement_id: prev.netelement_id.clone(),
position: best_from_side,
};
let to_side = NetelementSide {
netelement_id: next.netelement_id.clone(),
position: best_to_side,
};
let bridge_ne_ids = trace_intermediate_netelements(
graph,
node_map,
&from_side,
&to_side,
&prev.netelement_id,
&next.netelement_id,
);
if !bridge_ne_ids.is_empty() && cursor + 1 < segments.len() {
let last_bridge = bridge_ne_ids.last().unwrap();
let after_target = &segments[cursor + 1];
if !triple_is_consistent(
last_bridge,
&next.netelement_id,
&after_target.netelement_id,
graph,
node_map,
) {
let warn = format!(
"Gap fill: removed {} (U-turn: bridge route via {} \
creates direction violation with successor {})",
next.netelement_id, last_bridge, after_target.netelement_id,
);
warnings.push(warn.clone());
gap_fills.push(GapFill {
pair_index: cursor - 1,
from_netelement_id: prev.netelement_id.clone(),
to_netelement_id: next.netelement_id.clone(),
route_found: false,
inserted_netelements: vec![],
warning: warn,
});
if let Some(last) = result.last_mut() {
if last.gnss_end_index < next.gnss_end_index {
last.gnss_end_index = next.gnss_end_index;
}
}
cursor += 1;
continue;
}
}
if !bridge_ne_ids.is_empty() {
let gnss_idx = result
.last()
.map_or(prev.gnss_end_index, |r| r.gnss_end_index);
let mut inserted = Vec::new();
for ne_id in &bridge_ne_ids {
if netelement_index.contains_key(ne_id) {
if let Ok(bridge) = AssociatedNetElement::new(
ne_id.clone(),
1.0,
0.0,
1.0,
gnss_idx,
gnss_idx,
) {
result.push(bridge);
inserted.push(ne_id.clone());
}
}
}
let warn = format!(
"Gap fill: inserted {} bridge NE(s) between {} and {}: [{}]",
inserted.len(),
prev.netelement_id,
next.netelement_id,
inserted.join(", "),
);
warnings.push(warn.clone());
gap_fills.push(GapFill {
pair_index: cursor - 1,
from_netelement_id: prev.netelement_id.clone(),
to_netelement_id: next.netelement_id.clone(),
route_found: true,
inserted_netelements: inserted,
warning: warn,
});
} else {
gap_fills.push(GapFill {
pair_index: cursor - 1,
from_netelement_id: prev.netelement_id.clone(),
to_netelement_id: next.netelement_id.clone(),
route_found: true,
inserted_netelements: vec![],
warning: String::new(),
});
}
} else {
let warn = format!(
"Gap fill: no route found between {} and {}",
prev.netelement_id, next.netelement_id,
);
warnings.push(warn.clone());
gap_fills.push(GapFill {
pair_index: cursor - 1,
from_netelement_id: prev.netelement_id.clone(),
to_netelement_id: next.netelement_id.clone(),
route_found: false,
inserted_netelements: vec![],
warning: warn,
});
}
result.push(next.clone());
effective_prev = cursor;
cursor += 1;
}
(result, warnings, gap_fills)
}
fn trace_intermediate_netelements(
graph: &DiGraph<NetelementSide, f64>,
node_map: &HashMap<NetelementSide, NodeIndex>,
from: &NetelementSide,
to: &NetelementSide,
from_ne_id: &str,
to_ne_id: &str,
) -> Vec<String> {
let Some(path_nodes) = shortest_path_route(graph, node_map, from, to) else {
return vec![];
};
let mut ne_ids: Vec<String> = Vec::new();
let mut seen = std::collections::HashSet::new();
for nidx in &path_nodes {
let ne_side = &graph[*nidx];
if ne_side.netelement_id != from_ne_id
&& ne_side.netelement_id != to_ne_id
&& seen.insert(ne_side.netelement_id.clone())
{
ne_ids.push(ne_side.netelement_id.clone());
}
}
ne_ids
}
#[cfg(test)]
mod tests {
use super::*;
use geo::{LineString, Point};
fn make_ne(id: &str, coords: Vec<(f64, f64)>) -> Netelement {
Netelement {
id: id.to_string(),
geometry: LineString::from(coords),
crs: "EPSG:4326".to_string(),
}
}
fn make_cand(ne_id: &str, intrinsic: f64, lon: f64, lat: f64) -> CandidateNetElement {
CandidateNetElement {
netelement_id: ne_id.to_string(),
distance_meters: 5.0,
intrinsic_coordinate: intrinsic,
projected_point: Point::new(lon, lat),
}
}
#[test]
fn test_viterbi_simple_trellis() {
let netelements = vec![
make_ne("A", vec![(3.0, 50.0), (3.001, 50.0)]),
make_ne("B", vec![(3.001, 50.0), (3.002, 50.0)]),
];
let netelement_index: HashMap<String, usize> = [("A".to_string(), 0), ("B".to_string(), 1)]
.into_iter()
.collect();
use crate::models::NetRelation;
let netrelations = vec![NetRelation::new(
"NR1".to_string(),
"A".to_string(),
"B".to_string(),
1,
0,
true,
true,
)
.unwrap()];
let (graph, node_map) =
crate::path::graph::build_topology_graph(&netelements, &netrelations).unwrap();
let mut cache = ShortestPathCache::new();
let config = PathConfig::default();
let position_candidates = vec![
vec![make_cand("A", 0.2, 3.0002, 50.0)],
vec![make_cand("A", 0.8, 3.0008, 50.0)],
vec![
make_cand("A", 0.99, 3.00099, 50.0),
make_cand("B", 0.1, 3.0011, 50.0),
],
];
let position_probabilities = vec![
vec![0.9],
vec![0.85],
vec![0.3, 0.9], ];
let result = viterbi_decode(
&position_candidates,
&position_probabilities,
&netelements,
&netelement_index,
&graph,
&node_map,
&mut cache,
&config,
)
.unwrap();
assert_eq!(result.subsequences.len(), 1);
let seq = &result.subsequences[0];
assert_eq!(seq.states.len(), 3);
assert_eq!(seq.states[0], (0, 0));
assert_eq!(seq.states[1], (1, 0));
assert_eq!(seq.states[2], (2, 1));
}
#[test]
fn test_viterbi_no_break_on_disconnected() {
let netelements = vec![
make_ne("A", vec![(3.0, 50.0), (3.001, 50.0)]),
make_ne("B", vec![(4.0, 51.0), (4.001, 51.0)]),
];
let netelement_index: HashMap<String, usize> = [("A".to_string(), 0), ("B".to_string(), 1)]
.into_iter()
.collect();
let (graph, node_map) =
crate::path::graph::build_topology_graph(&netelements, &[]).unwrap();
let mut cache = ShortestPathCache::new();
let config = PathConfig::default();
let position_candidates = vec![
vec![make_cand("A", 0.5, 3.0005, 50.0)],
vec![make_cand("B", 0.5, 4.0005, 51.0)],
];
let position_probabilities = vec![vec![0.9], vec![0.9]];
let result = viterbi_decode(
&position_candidates,
&position_probabilities,
&netelements,
&netelement_index,
&graph,
&node_map,
&mut cache,
&config,
)
.unwrap();
assert_eq!(result.subsequences.len(), 1);
assert_eq!(result.subsequences[0].states.len(), 2);
assert_eq!(
position_candidates[result.subsequences[0].states[0].0]
[result.subsequences[0].states[0].1]
.netelement_id,
"A"
);
assert_eq!(
position_candidates[result.subsequences[0].states[1].0]
[result.subsequences[0].states[1].1]
.netelement_id,
"B"
);
}
#[test]
fn test_viterbi_empty_input() {
let netelements: Vec<Netelement> = vec![];
let netelement_index = HashMap::new();
let graph = DiGraph::new();
let node_map = HashMap::new();
let mut cache = ShortestPathCache::new();
let config = PathConfig::default();
let result = viterbi_decode(
&[],
&[],
&netelements,
&netelement_index,
&graph,
&node_map,
&mut cache,
&config,
)
.unwrap();
assert!(result.subsequences.is_empty());
}
#[test]
fn test_safe_ln() {
assert_eq!(safe_ln(0.0), f64::NEG_INFINITY);
assert_eq!(safe_ln(-1.0), f64::NEG_INFINITY);
assert!((safe_ln(1.0) - 0.0).abs() < 1e-12);
assert!((safe_ln(std::f64::consts::E) - 1.0).abs() < 1e-12);
}
}