rustygraph/core/
visibility_graph.rs

1//! Visibility graph construction and representation.
2//!
3//! This module provides types for building and working with visibility graphs
4//! from time series data. It supports both natural and horizontal visibility algorithms.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use rustygraph::{TimeSeries, VisibilityGraph};
10//!
11//! let series = TimeSeries::from_raw(vec![1.0, 3.0, 2.0, 4.0, 1.0]).unwrap();
12//! let graph = VisibilityGraph::from_series(&series)
13//!     .natural_visibility()
14//!     .unwrap();
15//!
16//! println!("Number of edges: {}", graph.edges().len());
17//! ```
18
19use crate::core::time_series::TimeSeries;
20use crate::core::features::FeatureSet;
21use std::collections::HashMap;
22use std::fmt;
23
24/// Visibility graph representation with node features.
25///
26/// A visibility graph represents temporal connectivity patterns in time series data.
27/// Each data point becomes a node, and edges connect points that have "visibility"
28/// to each other according to the chosen algorithm.
29///
30/// # Examples
31///
32/// ## Basic usage
33///
34/// ```rust
35/// use rustygraph::{TimeSeries, VisibilityGraph};
36///
37/// let series = TimeSeries::from_raw(vec![1.0, 3.0, 2.0, 4.0, 1.0]).unwrap();
38/// let graph = VisibilityGraph::from_series(&series)
39///     .natural_visibility()
40///     .unwrap();
41///
42/// // Access graph properties
43/// println!("Nodes: {}", graph.node_count);
44/// println!("Edges: {}", graph.edges().len());
45/// println!("Degree sequence: {:?}", graph.degree_sequence());
46/// ```
47///
48/// ## With features
49///
50/// ```rust
51/// use rustygraph::{TimeSeries, VisibilityGraph, FeatureSet, BuiltinFeature};
52///
53/// let series = TimeSeries::from_raw(vec![1.0, 3.0, 2.0, 4.0]).unwrap();
54/// let graph = VisibilityGraph::from_series(&series)
55///     .with_features(
56///         FeatureSet::new()
57///             .add_builtin(BuiltinFeature::DeltaForward)
58///             .add_builtin(BuiltinFeature::LocalSlope)
59///     )
60///     .natural_visibility()
61///     .unwrap();
62/// ```
63#[derive(Debug, Clone)]
64pub struct VisibilityGraph<T> {
65    /// Number of nodes (data points)
66    pub node_count: usize,
67    /// Graph edges as (source, target) pairs
68    pub(crate) edges: HashMap<(usize, usize), f64>,
69    /// Adjacency list representation
70    adjacency: Vec<Vec<f64>>,
71    /// Computed features for each node
72    pub node_features: Vec<HashMap<String, T>>,
73    /// Whether the graph is directed
74    pub directed: bool,
75}
76
77/// Direction mode for visibility graphs.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum GraphDirection {
80    /// Undirected graph (default) - edges go both ways
81    Undirected,
82    /// Directed graph - edges only from earlier to later time points
83    Directed,
84}
85
86impl<T> VisibilityGraph<T> {
87    /// Creates a visibility graph builder from a time series.
88    ///
89    /// This is the main entry point for constructing visibility graphs.
90    /// Use the returned builder to configure options and select an algorithm.
91    ///
92    /// # Arguments
93    ///
94    /// - `series`: Reference to time series data
95    ///
96    /// # Returns
97    ///
98    /// A [`VisibilityGraphBuilder`] for configuration
99    ///
100    /// # Examples
101    ///
102    /// ```rust
103    /// use rustygraph::{TimeSeries, VisibilityGraph};
104    ///
105    /// let series = TimeSeries::from_raw(vec![1.0, 2.0, 3.0]).unwrap();
106    /// let graph = VisibilityGraph::from_series(&series)
107    ///     .natural_visibility()
108    ///     .unwrap();
109    /// ```
110    pub fn from_series(series: &TimeSeries<T>) -> VisibilityGraphBuilder<'_, T> {
111        VisibilityGraphBuilder {
112            series,
113            feature_set: None,
114            direction: GraphDirection::Undirected,
115        }
116    }
117
118    /// Returns all edges in the graph.
119    ///
120    /// Each edge is represented as a tuple `(source, target)` where both
121    /// values are node indices.
122    ///
123    /// # Examples
124    ///
125    /// ```rust
126    /// use rustygraph::{TimeSeries, VisibilityGraph};
127    ///
128    /// let series = TimeSeries::from_raw(vec![1.0, 3.0, 2.0]).unwrap();
129    /// let graph = VisibilityGraph::from_series(&series)
130    ///     .natural_visibility()
131    ///     .unwrap();
132    ///
133    /// for (&(src, dst), &weight) in graph.edges() {
134    ///     println!("{} -> {}: {}", src, dst, weight);
135    /// }
136    /// ```
137    pub fn edges(&self) -> &HashMap<(usize, usize), f64> {
138        &self.edges
139    }
140
141    /// Returns the adjacency list for a specific node.
142    ///
143    /// # Arguments
144    ///
145    /// - `node`: Node index
146    ///
147    /// # Returns
148    ///
149    /// Slice of adjacent node indices, or `None` if node doesn't exist
150    ///
151    /// # Examples
152    ///
153    /// ```rust
154    /// use rustygraph::{TimeSeries, VisibilityGraph};
155    ///
156    /// let series = TimeSeries::from_raw(vec![1.0, 3.0, 2.0, 4.0]).unwrap();
157    /// let graph = VisibilityGraph::from_series(&series)
158    ///     .natural_visibility()
159    ///     .unwrap();
160    ///
161    /// if let Some(neighbors) = graph.neighbors(0) {
162    ///     println!("Node 0 is connected to: {:?}", neighbors);
163    /// }
164    /// ```
165    pub fn neighbors(&self, node: usize) -> Option<&[f64]> {
166        self.adjacency.get(node).map(|v| v.as_slice())
167    }
168
169    /// Checks if an edge exists between two nodes.
170    ///
171    /// # Arguments
172    ///
173    /// - `from`: Source node index
174    /// - `to`: Target node index
175    ///
176    /// # Returns
177    ///
178    /// `true` if an edge exists, `false` otherwise
179    pub fn has_edge(&self, from: usize, to: usize) -> bool {
180        self.edges.contains_key(&(from, to)) ||
181        (!self.directed && self.edges.contains_key(&(to, from)))
182    }
183
184    /// Returns the indices of neighboring nodes.
185    ///
186    /// # Arguments
187    ///
188    /// - `node`: Node index
189    ///
190    /// # Returns
191    ///
192    /// Vector of neighbor node indices
193    pub fn neighbor_indices(&self, node: usize) -> Vec<usize> {
194        self.edges
195            .keys()
196            .filter_map(|(i, j)| {
197                if *i == node {
198                    Some(*j)
199                } else if !self.directed && *j == node {
200                    Some(*i)
201                } else {
202                    None
203                }
204            })
205            .collect()
206    }
207
208    /// Returns the degree of a node (number of connections).
209    ///
210    /// # Arguments
211    ///
212    /// - `node`: Node index
213    ///
214    /// # Returns
215    ///
216    /// The number of edges connected to this node, or `None` if node doesn't exist
217    ///
218    /// # Examples
219    ///
220    /// ```rust
221    /// use rustygraph::{TimeSeries, VisibilityGraph};
222    ///
223    /// let series = TimeSeries::from_raw(vec![1.0, 3.0, 2.0, 4.0]).unwrap();
224    /// let graph = VisibilityGraph::from_series(&series)
225    ///     .natural_visibility()
226    ///     .unwrap();
227    ///
228    /// if let Some(degree) = graph.degree(0) {
229    ///     println!("Node 0 has degree {}", degree);
230    /// }
231    /// ```
232    pub fn degree(&self, node: usize) -> Option<usize> {
233        self.adjacency.get(node).map(|v| v.len())
234    }
235
236    /// Returns the degree sequence of all nodes.
237    ///
238    /// The degree sequence is a vector where each element is the degree
239    /// of the corresponding node.
240    ///
241    /// # Examples
242    ///
243    /// ```rust
244    /// use rustygraph::{TimeSeries, VisibilityGraph};
245    ///
246    /// let series = TimeSeries::from_raw(vec![1.0, 3.0, 2.0, 4.0]).unwrap();
247    /// let graph = VisibilityGraph::from_series(&series)
248    ///     .natural_visibility()
249    ///     .unwrap();
250    ///
251    /// let degrees = graph.degree_sequence();
252    /// println!("Degree sequence: {:?}", degrees);
253    /// ```
254    pub fn degree_sequence(&self) -> Vec<usize> {
255        self.adjacency.iter().map(|v| v.len()).collect()
256    }
257
258    /// Returns computed features for a specific node.
259    ///
260    /// # Arguments
261    ///
262    /// - `node`: Node index
263    ///
264    /// # Returns
265    ///
266    /// A map of feature names to values, or `None` if node doesn't exist
267    /// or no features were computed
268    ///
269    /// # Examples
270    ///
271    /// ```rust
272    /// use rustygraph::{TimeSeries, VisibilityGraph, FeatureSet, BuiltinFeature};
273    ///
274    /// let series = TimeSeries::from_raw(vec![1.0, 3.0, 2.0]).unwrap();
275    /// let graph = VisibilityGraph::from_series(&series)
276    ///     .with_features(
277    ///         FeatureSet::new().add_builtin(BuiltinFeature::DeltaForward)
278    ///     )
279    ///     .natural_visibility()
280    ///     .unwrap();
281    ///
282    /// if let Some(features) = graph.node_features(0) {
283    ///     for (name, value) in features {
284    ///         println!("{}: {}", name, value);
285    ///     }
286    /// }
287    /// ```
288    pub fn node_features(&self, node: usize) -> Option<&HashMap<String, T>> {
289        self.node_features.get(node)
290    }
291
292    /// Exports the graph to an adjacency matrix.
293    ///
294    /// The adjacency matrix is a square boolean matrix where `matrix[i][j]`
295    /// is `true` if there is an edge from node `i` to node `j`.
296    ///
297    /// # Examples
298    ///
299    /// ```rust
300    /// use rustygraph::{TimeSeries, VisibilityGraph};
301    ///
302    /// let series = TimeSeries::from_raw(vec![1.0, 3.0, 2.0]).unwrap();
303    /// let graph = VisibilityGraph::from_series(&series)
304    ///     .natural_visibility()
305    ///     .unwrap();
306    ///
307    /// let matrix = graph.to_adjacency_matrix();
308    /// for row in &matrix {
309    ///     println!("{:?}", row);
310    /// }
311    /// ```
312    pub fn to_adjacency_matrix(&self) -> Vec<Vec<f64>> {
313        let mut matrix = vec![vec![0.0; self.node_count]; self.node_count];
314        for &(src, dst) in self.edges.keys() {
315            matrix[src][dst] = self.edges[&(src, dst)];
316        }
317        matrix
318    }
319}
320
321/// Builder for constructing visibility graphs with options.
322///
323/// This builder allows you to configure features and select the visibility
324/// algorithm before constructing the graph.
325///
326/// # Examples
327///
328/// ```rust
329/// use rustygraph::{TimeSeries, VisibilityGraph, FeatureSet, BuiltinFeature};
330///
331/// let series = TimeSeries::from_raw(vec![1.0, 2.0, 3.0]).unwrap();
332/// let graph = VisibilityGraph::from_series(&series)
333///     .with_features(
334///         FeatureSet::new().add_builtin(BuiltinFeature::DeltaForward)
335///     )
336///     .natural_visibility()
337///     .unwrap();
338/// ```
339pub struct VisibilityGraphBuilder<'a, T> {
340    #[allow(dead_code)] // Will be used when algorithms are implemented
341    series: &'a TimeSeries<T>,
342    feature_set: Option<FeatureSet<T>>,
343    direction: GraphDirection,
344}
345
346impl<'a, T> VisibilityGraphBuilder<'a, T>
347where
348    T: Copy + PartialOrd + Into<f64>,
349{
350    /// Specifies features to compute for each node.
351    ///
352    /// Features are computed after the graph structure is built.
353    ///
354    /// # Arguments
355    ///
356    /// - `features`: A configured [`FeatureSet`]
357    ///
358    /// # Examples
359    ///
360    /// ```rust
361    /// use rustygraph::{TimeSeries, VisibilityGraph, FeatureSet, BuiltinFeature};
362    ///
363    /// let series = TimeSeries::from_raw(vec![1.0, 2.0, 3.0]).unwrap();
364    /// let graph = VisibilityGraph::from_series(&series)
365    ///     .with_features(
366    ///         FeatureSet::new()
367    ///             .add_builtin(BuiltinFeature::DeltaForward)
368    ///             .add_builtin(BuiltinFeature::LocalSlope)
369    ///     )
370    ///     .natural_visibility()
371    ///     .unwrap();
372    /// ```
373    pub fn with_features(mut self, features: FeatureSet<T>) -> Self {
374        self.feature_set = Some(features);
375        self
376    }
377
378    /// Sets the graph direction mode.
379    ///
380    /// # Arguments
381    ///
382    /// - `direction`: Whether the graph should be directed or undirected
383    ///
384    /// # Examples
385    ///
386    /// ```rust
387    /// use rustygraph::{TimeSeries, VisibilityGraph, GraphDirection};
388    ///
389    /// let series = TimeSeries::from_raw(vec![1.0, 2.0, 3.0]).unwrap();
390    /// let graph = VisibilityGraph::from_series(&series)
391    ///     .with_direction(GraphDirection::Directed)
392    ///     .natural_visibility()
393    ///     .unwrap();
394    /// ```
395    pub fn with_direction(mut self, direction: GraphDirection) -> Self {
396        self.direction = direction;
397        self
398    }
399
400    /// Constructs a natural visibility graph.
401    ///
402    /// In a natural visibility graph, two nodes (i, yi) and (j, yj) are connected
403    /// if all intermediate points (k, yk) satisfy the visibility criterion:
404    ///
405    /// `yk < yi + (yj - yi) * (tk - ti) / (tj - ti)`
406    ///
407    /// # Algorithm
408    ///
409    /// Uses a monotonic stack optimization for O(n) complexity per node.
410    ///
411    /// # Errors
412    ///
413    /// Returns [`GraphError`] if the time series is empty or feature computation fails.
414    ///
415    /// # Examples
416    ///
417    /// ```rust
418    /// use rustygraph::{TimeSeries, VisibilityGraph};
419    ///
420    /// let series = TimeSeries::from_raw(vec![1.0, 3.0, 2.0, 4.0, 1.0]).unwrap();
421    /// let graph = VisibilityGraph::from_series(&series)
422    ///     .natural_visibility()
423    ///     .unwrap();
424    /// ```
425    pub fn natural_visibility(self) -> Result<VisibilityGraph<T>, GraphError>
426    where
427        T: Copy + PartialOrd + Into<f64> + std::ops::Add<Output = T> + std::ops::Sub<Output = T>
428           + std::ops::Mul<Output = T> + std::ops::Div<Output = T> + From<f64> + Send + Sync,
429    {
430        use crate::core::algorithms::edges::{VisibilityEdges as create_visibility_edges, VisibilityType};
431
432        // Check for empty series
433        if self.series.is_empty() {
434            return Err(GraphError::EmptyTimeSeries);
435        }
436
437        // Check for all missing values
438        if self.series.values.iter().all(|v| v.is_none()) {
439            return Err(GraphError::AllValuesMissing);
440        }
441
442        // Compute edges using natural visibility algorithm
443        // Use parallel computation when available (significant speedup for large graphs)
444        let edges: HashMap<(usize, usize), f64> = {
445            let edge_computer = create_visibility_edges::new(
446                self.series,
447                VisibilityType::Natural,
448                |_, _, _, _| 1.0
449            );
450
451            #[cfg(feature = "parallel")]
452            {
453                edge_computer.compute_edges_parallel()
454            }
455
456            #[cfg(not(feature = "parallel"))]
457            {
458                edge_computer.compute_edges()
459            }
460        };
461
462        // Compute adjacency list
463        let directed = matches!(self.direction, GraphDirection::Directed);
464        let adj: Vec<Vec<f64>> = build_adjacency_list(self.series.len(), &edges, directed);
465
466        // Compute node features if feature set is provided
467        let node_features = if let Some(feature_set) = self.feature_set {
468            compute_node_features(&self.series.values, &feature_set)
469        } else {
470            vec![]
471        };
472
473        Ok(VisibilityGraph {
474            node_count: self.series.len(),
475            edges,
476            adjacency: adj,
477            node_features,
478            directed,
479        })
480    }
481
482    /// Constructs a horizontal visibility graph.
483    ///
484    /// In a horizontal visibility graph, two nodes are connected if all
485    /// intermediate values are strictly lower than both endpoints.
486    ///
487    /// # Algorithm
488    ///
489    /// Uses a linear scan approach with O(n) average case complexity.
490    ///
491    /// # Errors
492    ///
493    /// Returns [`GraphError`] if the time series is empty or feature computation fails.
494    ///
495    /// # Examples
496    ///
497    /// ```rust
498    /// use rustygraph::{TimeSeries, VisibilityGraph};
499    ///
500    /// let series = TimeSeries::from_raw(vec![1.0, 3.0, 2.0, 4.0, 1.0]).unwrap();
501    /// let graph = VisibilityGraph::from_series(&series)
502    ///     .horizontal_visibility()
503    ///     .unwrap();
504    /// ```
505    pub fn horizontal_visibility(self) -> Result<VisibilityGraph<T>, GraphError>
506    where
507        T: Copy + PartialOrd + Into<f64> + std::ops::Add<Output = T> + std::ops::Sub<Output = T>
508           + std::ops::Mul<Output = T> + std::ops::Div<Output = T> + From<f64> + Send + Sync,
509    {
510        use crate::core::algorithms::edges::{VisibilityEdges as create_visibility_edges, VisibilityType};
511
512        // Check for empty series
513        if self.series.is_empty() {
514            return Err(GraphError::EmptyTimeSeries);
515        }
516
517        // Check for all missing values
518        if self.series.values.iter().all(|v| v.is_none()) {
519            return Err(GraphError::AllValuesMissing);
520        }
521
522        // Compute edges using horizontal visibility algorithm
523        // Use parallel computation when available (significant speedup for large graphs)
524        let edges: HashMap<(usize, usize), f64> = {
525            let edge_computer = create_visibility_edges::new(
526                self.series,
527                VisibilityType::Horizontal,
528                |_, _, _, _| 1.0
529            );
530
531            #[cfg(feature = "parallel")]
532            {
533                edge_computer.compute_edges_parallel()
534            }
535
536            #[cfg(not(feature = "parallel"))]
537            {
538                edge_computer.compute_edges()
539            }
540        };
541
542        // Compute adjacency list
543        let directed = matches!(self.direction, GraphDirection::Directed);
544        let adj: Vec<Vec<f64>> = build_adjacency_list(self.series.len(), &edges, directed);
545
546        // Compute node features if feature set is provided
547        let node_features = if let Some(feature_set) = self.feature_set {
548            compute_node_features(&self.series.values, &feature_set)
549        } else {
550            vec![]
551        };
552
553        Ok(VisibilityGraph {
554            node_count: self.series.len(),
555            edges,
556            adjacency: adj,
557            node_features,
558            directed,
559        })
560    }
561}
562
563/// Errors during visibility graph construction.
564#[derive(Debug, Clone, PartialEq)]
565pub enum GraphError {
566    /// Time series is empty
567    EmptyTimeSeries,
568    /// Feature computation failed
569    FeatureComputationFailed {
570        /// Name of the feature that failed
571        feature: String,
572        /// Node index where failure occurred
573        node: usize,
574    },
575    /// All values are missing
576    AllValuesMissing,
577}
578
579impl fmt::Display for GraphError {
580    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
581        match self {
582            GraphError::EmptyTimeSeries => write!(f, "Time series is empty"),
583            GraphError::FeatureComputationFailed { feature, node } => {
584                write!(f, "Feature '{}' computation failed at node {}", feature, node)
585            }
586            GraphError::AllValuesMissing => write!(f, "All values are missing"),
587        }
588    }
589}
590
591
592fn build_adjacency_list(node_count: usize, edges: &HashMap<(usize, usize), f64>, directed: bool) -> Vec<Vec<f64>> {
593    let mut adjacency = vec![Vec::new(); node_count];
594    for &(src, dst) in edges.keys() {
595        let weight = edges.get(&(src, dst)).copied().unwrap_or(0.0);
596        adjacency[src].push(weight);
597        if !directed {
598            adjacency[dst].push(weight); // Add reverse edge for undirected
599        }
600    }
601    adjacency
602}
603
604/// Computes all features for all nodes in the series.
605///
606/// This function automatically uses parallel computation when the `parallel` feature is enabled,
607/// otherwise falls back to sequential computation.
608pub(crate) fn compute_node_features<T>(
609    series: &[Option<T>],
610    feature_set: &FeatureSet<T>,
611) -> Vec<HashMap<String, T>>
612where
613    T: Copy + PartialOrd + std::ops::Add<Output = T> + std::ops::Sub<Output = T>
614       + std::ops::Mul<Output = T> + std::ops::Div<Output = T> + From<f64> + Into<f64>
615       + Send + Sync,
616{
617    // Use parallel implementation when parallel feature is enabled
618    #[cfg(feature = "parallel")]
619    {
620        crate::performance::parallel::compute_node_features_parallel(series, feature_set)
621    }
622
623    // Fall back to sequential implementation
624    #[cfg(not(feature = "parallel"))]
625    {
626        let mut node_features = Vec::with_capacity(series.len());
627
628        for i in 0..series.len() {
629            let mut features = HashMap::new();
630
631            // Compute each feature for this node
632            for feature in &feature_set.features {
633                if let Some(value) = feature.compute(series, i, &feature_set.missing_strategy) {
634                    features.insert(feature.name().to_string(), value);
635                }
636            }
637
638            node_features.push(features);
639        }
640
641        node_features
642    }
643}
644
645impl std::error::Error for GraphError {}
646