use crate::core::time_series::TimeSeries;
use crate::core::features::FeatureSet;
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone)]
pub struct VisibilityGraph<T> {
pub node_count: usize,
pub(crate) edges: HashMap<(usize, usize), f64>,
adjacency: Vec<Vec<f64>>,
pub node_features: Vec<HashMap<String, T>>,
pub directed: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GraphDirection {
Undirected,
Directed,
}
impl<T> VisibilityGraph<T> {
pub fn from_series(series: &TimeSeries<T>) -> VisibilityGraphBuilder<'_, T> {
VisibilityGraphBuilder {
series,
feature_set: None,
direction: GraphDirection::Undirected,
}
}
pub fn edges(&self) -> &HashMap<(usize, usize), f64> {
&self.edges
}
pub fn neighbors(&self, node: usize) -> Option<&[f64]> {
self.adjacency.get(node).map(|v| v.as_slice())
}
pub fn has_edge(&self, from: usize, to: usize) -> bool {
self.edges.contains_key(&(from, to)) ||
(!self.directed && self.edges.contains_key(&(to, from)))
}
pub fn neighbor_indices(&self, node: usize) -> Vec<usize> {
self.edges
.keys()
.filter_map(|(i, j)| {
if *i == node {
Some(*j)
} else if !self.directed && *j == node {
Some(*i)
} else {
None
}
})
.collect()
}
pub fn degree(&self, node: usize) -> Option<usize> {
self.adjacency.get(node).map(|v| v.len())
}
pub fn degree_sequence(&self) -> Vec<usize> {
self.adjacency.iter().map(|v| v.len()).collect()
}
pub fn node_features(&self, node: usize) -> Option<&HashMap<String, T>> {
self.node_features.get(node)
}
pub fn to_adjacency_matrix(&self) -> Vec<Vec<f64>> {
let mut matrix = vec![vec![0.0; self.node_count]; self.node_count];
for &(src, dst) in self.edges.keys() {
matrix[src][dst] = self.edges[&(src, dst)];
}
matrix
}
}
pub struct VisibilityGraphBuilder<'a, T> {
#[allow(dead_code)] series: &'a TimeSeries<T>,
feature_set: Option<FeatureSet<T>>,
direction: GraphDirection,
}
impl<'a, T> VisibilityGraphBuilder<'a, T>
where
T: Copy + PartialOrd + Into<f64>,
{
pub fn with_features(mut self, features: FeatureSet<T>) -> Self {
self.feature_set = Some(features);
self
}
pub fn with_direction(mut self, direction: GraphDirection) -> Self {
self.direction = direction;
self
}
pub fn natural_visibility(self) -> Result<VisibilityGraph<T>, GraphError>
where
T: Copy + PartialOrd + Into<f64> + std::ops::Add<Output = T> + std::ops::Sub<Output = T>
+ std::ops::Mul<Output = T> + std::ops::Div<Output = T> + From<f64> + Send + Sync,
{
use crate::core::algorithms::edges::{VisibilityEdges as create_visibility_edges, VisibilityType};
if self.series.is_empty() {
return Err(GraphError::EmptyTimeSeries);
}
if self.series.values.iter().all(|v| v.is_none()) {
return Err(GraphError::AllValuesMissing);
}
let edges: HashMap<(usize, usize), f64> = {
let edge_computer = create_visibility_edges::new(
self.series,
VisibilityType::Natural,
|_, _, _, _| 1.0
);
#[cfg(feature = "parallel")]
{
edge_computer.compute_edges_parallel()
}
#[cfg(not(feature = "parallel"))]
{
edge_computer.compute_edges()
}
};
let directed = matches!(self.direction, GraphDirection::Directed);
let adj: Vec<Vec<f64>> = build_adjacency_list(self.series.len(), &edges, directed);
let node_features = if let Some(feature_set) = self.feature_set {
compute_node_features(&self.series.values, &feature_set)
} else {
vec![]
};
Ok(VisibilityGraph {
node_count: self.series.len(),
edges,
adjacency: adj,
node_features,
directed,
})
}
pub fn horizontal_visibility(self) -> Result<VisibilityGraph<T>, GraphError>
where
T: Copy + PartialOrd + Into<f64> + std::ops::Add<Output = T> + std::ops::Sub<Output = T>
+ std::ops::Mul<Output = T> + std::ops::Div<Output = T> + From<f64> + Send + Sync,
{
use crate::core::algorithms::edges::{VisibilityEdges as create_visibility_edges, VisibilityType};
if self.series.is_empty() {
return Err(GraphError::EmptyTimeSeries);
}
if self.series.values.iter().all(|v| v.is_none()) {
return Err(GraphError::AllValuesMissing);
}
let edges: HashMap<(usize, usize), f64> = {
let edge_computer = create_visibility_edges::new(
self.series,
VisibilityType::Horizontal,
|_, _, _, _| 1.0
);
#[cfg(feature = "parallel")]
{
edge_computer.compute_edges_parallel()
}
#[cfg(not(feature = "parallel"))]
{
edge_computer.compute_edges()
}
};
let directed = matches!(self.direction, GraphDirection::Directed);
let adj: Vec<Vec<f64>> = build_adjacency_list(self.series.len(), &edges, directed);
let node_features = if let Some(feature_set) = self.feature_set {
compute_node_features(&self.series.values, &feature_set)
} else {
vec![]
};
Ok(VisibilityGraph {
node_count: self.series.len(),
edges,
adjacency: adj,
node_features,
directed,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum GraphError {
EmptyTimeSeries,
FeatureComputationFailed {
feature: String,
node: usize,
},
AllValuesMissing,
}
impl fmt::Display for GraphError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GraphError::EmptyTimeSeries => write!(f, "Time series is empty"),
GraphError::FeatureComputationFailed { feature, node } => {
write!(f, "Feature '{}' computation failed at node {}", feature, node)
}
GraphError::AllValuesMissing => write!(f, "All values are missing"),
}
}
}
fn build_adjacency_list(node_count: usize, edges: &HashMap<(usize, usize), f64>, directed: bool) -> Vec<Vec<f64>> {
let mut adjacency = vec![Vec::new(); node_count];
for &(src, dst) in edges.keys() {
let weight = edges.get(&(src, dst)).copied().unwrap_or(0.0);
adjacency[src].push(weight);
if !directed {
adjacency[dst].push(weight); }
}
adjacency
}
pub(crate) fn compute_node_features<T>(
series: &[Option<T>],
feature_set: &FeatureSet<T>,
) -> Vec<HashMap<String, T>>
where
T: Copy + PartialOrd + std::ops::Add<Output = T> + std::ops::Sub<Output = T>
+ std::ops::Mul<Output = T> + std::ops::Div<Output = T> + From<f64> + Into<f64>
+ Send + Sync,
{
#[cfg(feature = "parallel")]
{
crate::performance::parallel::compute_node_features_parallel(series, feature_set)
}
#[cfg(not(feature = "parallel"))]
{
let mut node_features = Vec::with_capacity(series.len());
for i in 0..series.len() {
let mut features = HashMap::new();
for feature in &feature_set.features {
if let Some(value) = feature.compute(series, i, &feature_set.missing_strategy) {
features.insert(feature.name().to_string(), value);
}
}
node_features.push(features);
}
node_features
}
}
impl std::error::Error for GraphError {}