catlog/one/
path.rs

1//! Paths in graphs and categories.
2//!
3//! The central data type is [`Path`], a path of arbitrary finite length. In
4//! addition, this module provides data types for ["short paths"](`ShortPath`) and
5//! [path equations](`PathEq`).
6
7use std::ops::Range;
8use std::{collections::HashSet, hash::Hash};
9
10use derive_more::{Constructor, From};
11use itertools::{Either, Itertools};
12use nonempty::{NonEmpty, nonempty};
13
14#[cfg(feature = "serde")]
15use serde::{Deserialize, Serialize};
16#[cfg(feature = "serde-wasm")]
17use tsify::Tsify;
18
19use super::graph::{Graph, ReflexiveGraph};
20use crate::validate;
21use crate::zero::QualifiedName;
22
23/// A path in a [graph](Graph) or [category](crate::one::category::Category).
24///
25/// This definition by cases can be compared with the perhaps more obvious
26/// definition:
27///
28/// ```
29/// struct Path<V, E> {
30/// start: V,
31/// end: V, // Optional: more symmetric but also more redundant.
32/// seq: Vec<E>,
33/// }
34/// ```
35///
36/// Not only does the single struct store redundant (hence possibly inconsistent)
37/// information when the sequence of edges is nonempty, one will often need to do a
38/// case analysis on the edge sequence anyway to determine whether, say,
39/// [`reduce`](std::iter::Iterator::reduce) returns a non-null value. Thus, it seems
40/// better to reify the two cases in the data structure itself.
41#[derive(Clone, Debug, PartialEq, Eq, Hash)]
42pub enum Path<V, E> {
43    /// The identity, or empty, path at a vertex.
44    Id(V),
45
46    /// A nontrivial path, comprising a *non-empty* vector of consecutive edges.
47    Seq(NonEmpty<E>),
48}
49
50/// A path whose vertices and edges are qualified names.
51pub type QualifiedPath = Path<QualifiedName, QualifiedName>;
52
53/// A path in a graph with skeletal vertex and edge sets.
54pub type SkelPath = Path<usize, usize>;
55
56/// Converts an edge into a path of length one.
57impl<V, E> From<E> for Path<V, E> {
58    fn from(e: E) -> Self {
59        Path::single(e)
60    }
61}
62
63/// Converts the path into an iterater over its edges.
64impl<V, E> IntoIterator for Path<V, E> {
65    type Item = E;
66    type IntoIter = Either<std::iter::Empty<E>, <NonEmpty<E> as IntoIterator>::IntoIter>;
67
68    fn into_iter(self) -> Self::IntoIter {
69        match self {
70            Path::Id(_) => Either::Left(std::iter::empty()),
71            Path::Seq(edges) => Either::Right(edges.into_iter()),
72        }
73    }
74}
75
76impl<V, E> Path<V, E> {
77    /// Constructs the empty or identity path.
78    pub fn empty(v: V) -> Self {
79        Path::Id(v)
80    }
81
82    /// Constructs a path with a single edge.
83    pub fn single(e: E) -> Self {
84        Path::Seq(NonEmpty::singleton(e))
85    }
86
87    /// Constructs a pair of consecutive edges, or path of length 2.
88    pub fn pair(e: E, f: E) -> Self {
89        Path::Seq(nonempty![e, f])
90    }
91
92    /// Constructs a path from an iterator over edges.
93    ///
94    /// Returns `None` if the iterator is empty.
95    pub fn collect<I>(iter: I) -> Option<Self>
96    where
97        I: IntoIterator<Item = E>,
98    {
99        NonEmpty::collect(iter).map(Path::Seq)
100    }
101
102    /// Constructs a path from a vector of edges.
103    ///
104    /// Returns `None` if the vector is empty.
105    pub fn from_vec(vec: Vec<E>) -> Option<Self> {
106        NonEmpty::from_vec(vec).map(Path::Seq)
107    }
108
109    /// Constructs a path by repeating an edge `n` times.
110    ///
111    /// The edge should have the same source and target, namely the first argument.
112    pub fn repeat_n(v: V, e: E, n: usize) -> Self
113    where
114        E: Clone,
115    {
116        Path::collect(std::iter::repeat_n(e, n)).unwrap_or_else(|| Path::empty(v))
117    }
118
119    /// Length of the path.
120    pub fn len(&self) -> usize {
121        match self {
122            Path::Id(_) => 0,
123            Path::Seq(edges) => edges.len(),
124        }
125    }
126
127    /// Is the path empty?
128    pub fn is_empty(&self) -> bool {
129        match self {
130            Path::Id(_) => true,
131            Path::Seq(_) => false,
132        }
133    }
134
135    /// Iterates over edges in the path, if any.
136    ///
137    /// This method is a one-sided inverse to [`Path::collect`].
138    pub fn iter(&self) -> impl Iterator<Item = &E> {
139        match self {
140            Path::Id(_) => Either::Left(std::iter::empty()),
141            Path::Seq(edges) => Either::Right(edges.iter()),
142        }
143    }
144
145    /// Extracts the unique edge in a path of length 1.
146    ///
147    /// This method is a one-sided inverse to [`Path::single`].
148    pub fn only(self) -> Option<E> {
149        match self {
150            Path::Id(_) => None,
151            Path::Seq(edges) => {
152                if edges.tail.is_empty() {
153                    Some(edges.head)
154                } else {
155                    None
156                }
157            }
158        }
159    }
160
161    /// Inserts an edge into the path at the given index.
162    pub fn insert(&mut self, index: usize, edge: E) {
163        if let Path::Seq(edges) = self {
164            edges.insert(index, edge);
165        } else {
166            *self = Path::single(edge);
167        }
168    }
169
170    /// Splices a path into another path at the given range of indices.
171    pub fn splice(self, range: Range<usize>, replace_with: Self) -> Self {
172        let new_path = if range.start == 0 && range.end == self.len() {
173            Some(replace_with)
174        } else if let Path::Seq(edges) = self {
175            let mut edges: Vec<_> = edges.into();
176            edges.splice(range, replace_with);
177            Path::from_vec(edges)
178        } else {
179            None
180        };
181        new_path.expect("Range of indices into path should be valid")
182    }
183
184    /// Source of the path in the given graph.
185    ///
186    /// Assumes that the path is [contained in](Path::contained_in) the graph.
187    pub fn src(&self, graph: &impl Graph<V = V, E = E>) -> V
188    where
189        V: Clone,
190    {
191        match self {
192            Path::Id(v) => v.clone(),
193            Path::Seq(edges) => graph.src(edges.first()),
194        }
195    }
196
197    /// Target of the path in the given graph.
198    ///
199    /// Assumes that the path is [contained in](Path::contained_in) the graph.
200    pub fn tgt(&self, graph: &impl Graph<V = V, E = E>) -> V
201    where
202        V: Clone,
203    {
204        match self {
205            Path::Id(v) => v.clone(),
206            Path::Seq(edges) => graph.tgt(edges.last()),
207        }
208    }
209
210    /// Extracts a subpath of a path in a graph.
211    ///
212    /// Panics if the range is invalid or an empty subpath would be inconsistent.
213    pub fn subpath(&self, graph: &impl Graph<V = V, E = E>, range: Range<usize>) -> Self
214    where
215        V: Eq + Clone,
216        E: Clone,
217    {
218        if let Path::Seq(edges) = self {
219            if range.is_empty() {
220                let index = range.start;
221                let v = if index == 0 {
222                    graph.src(edges.first())
223                } else if index == edges.len() {
224                    graph.tgt(edges.last())
225                } else if index < edges.len() {
226                    let (t, s) = (graph.tgt(&(*edges)[index - 1]), graph.src(&(*edges)[index]));
227                    assert!(t == s, "Inconsistent intermediate vertex in path");
228                    t
229                } else {
230                    panic!("Invalid index for empty subpath of path");
231                };
232                Path::Id(v)
233            } else {
234                let (start, end) = (range.start, range.end);
235                let iter = if start == 0 {
236                    let head = std::iter::once(edges.head.clone());
237                    let tail = edges.tail[0..(end - 1)].iter().cloned();
238                    Either::Left(head.chain(tail))
239                } else {
240                    Either::Right(edges.tail[(start - 1)..(end - 1)].iter().cloned())
241                };
242                Path::collect(iter).unwrap()
243            }
244        } else {
245            assert!(range.start == 0 && range.is_empty(), "Invalid subpath of empty path");
246            self.clone()
247        }
248    }
249
250    /// Replaces the subpath at the given range with a function of that subpath.
251    ///
252    /// Panics under the same conditions as [`subpath`](Self::subpath).
253    pub fn replace_subpath(
254        self,
255        graph: &impl Graph<V = V, E = E>,
256        range: Range<usize>,
257        f: impl FnOnce(Self) -> Self,
258    ) -> Self
259    where
260        V: Eq + Clone,
261        E: Clone,
262    {
263        let subpath = self.subpath(graph, range.clone());
264        self.splice(range, f(subpath))
265    }
266
267    /// Concatenates this path with another path in the graph.
268    ///
269    /// This methods *checks* that the two paths are compatible (the target of this
270    /// path equals the source of the other path) and it *assumes* that both paths
271    /// are contained in the graph, which should be checked with
272    /// [`contained_in`](Self::contained_in) if in doubt. Thus, when returned, the
273    /// concatenated path is also a valid path.
274    pub fn concat_in(self, graph: &impl Graph<V = V, E = E>, other: Self) -> Option<Self>
275    where
276        V: Eq + Clone,
277    {
278        if self.tgt(graph) != other.src(graph) {
279            return None;
280        }
281        let concatenated = match (self, other) {
282            (path, Path::Id(_)) => path,
283            (Path::Id(_), path) => path,
284            (Path::Seq(mut edges), Path::Seq(mut other_edges)) => {
285                edges.push(other_edges.head);
286                edges.append(&mut other_edges.tail);
287                Path::Seq(edges)
288            }
289        };
290        Some(concatenated)
291    }
292
293    /// Is the path contained in the given graph?
294    pub fn contained_in(&self, graph: &impl Graph<V = V, E = E>) -> bool
295    where
296        V: Eq,
297    {
298        match self {
299            Path::Id(v) => graph.has_vertex(v),
300            Path::Seq(edges) => {
301                // All the edges exist in the graph...
302                edges.iter().all(|e| graph.has_edge(e)) &&
303                // ...and their sources and target are compatible.
304                edges.iter().tuple_windows().all(|(e, f)| graph.tgt(e) == graph.src(f))
305            }
306        }
307    }
308
309    /// Returns whether the path is simple.
310    ///
311    /// On our definition, a path is **simple** if it has no repeated edges.
312    pub fn is_simple(&self) -> bool
313    where
314        E: Eq + Hash,
315    {
316        match self {
317            Path::Id(_) => true,
318            Path::Seq(edges) => {
319                let edges: HashSet<_> = edges.into_iter().collect();
320                edges.len() == self.len()
321            }
322        }
323    }
324
325    /// Reduces a path using functions on vertices and edges.
326    pub fn reduce(self, fv: impl FnOnce(V) -> E, fe: impl FnMut(E, E) -> E) -> E {
327        match self {
328            Path::Id(v) => fv(v),
329            // `reduce` cannot fail since edge sequence is nonempty.
330            Path::Seq(edges) => edges.into_iter().reduce(fe).unwrap(),
331        }
332    }
333
334    /// Maps a path over functions on vertices and edges.
335    pub fn map<CodV, CodE>(
336        self,
337        fv: impl FnOnce(V) -> CodV,
338        fe: impl FnMut(E) -> CodE,
339    ) -> Path<CodV, CodE> {
340        match self {
341            Path::Id(v) => Path::Id(fv(v)),
342            Path::Seq(edges) => Path::Seq(edges.map(fe)),
343        }
344    }
345
346    /// Maps and then reduces over a path.
347    ///
348    /// This equivalent to calling [`map`](Path::map) and then
349    /// [`reduce`](Path::reduce) but avoids allocating the intermediate path.
350    pub fn map_reduce<T>(
351        self,
352        fv: impl FnOnce(V) -> T,
353        fe: impl FnMut(E) -> T,
354        f: impl FnMut(T, T) -> T,
355    ) -> T {
356        match self {
357            Path::Id(v) => fv(v),
358            Path::Seq(edges) => edges.into_iter().map(fe).reduce(f).unwrap(),
359        }
360    }
361
362    /// Maps a path over partial functions on vertices and edges.
363    pub fn partial_map<CodV, CodE>(
364        self,
365        fv: impl FnOnce(V) -> Option<CodV>,
366        fe: impl FnMut(E) -> Option<CodE>,
367    ) -> Option<Path<CodV, CodE>> {
368        match self {
369            Path::Id(v) => Some(Path::Id(fv(v)?)),
370            Path::Seq(edges) => {
371                let edges: Option<Vec<_>> = edges.into_iter().map(fe).collect();
372                Path::from_vec(edges?)
373            }
374        }
375    }
376
377    /// Maps a path over fallible functions on vertices and edges.
378    pub fn try_map<CodV, CodE, Err>(
379        self,
380        fv: impl FnOnce(V) -> Result<CodV, Err>,
381        fe: impl FnMut(E) -> Result<CodE, Err>,
382    ) -> Result<Path<CodV, CodE>, Err> {
383        match self {
384            Path::Id(v) => Ok(Path::Id(fv(v)?)),
385            Path::Seq(edges) => {
386                let edges: Result<Vec<_>, _> = edges.into_iter().map(fe).collect();
387                Ok(Path::from_vec(edges?).unwrap())
388            }
389        }
390    }
391}
392
393impl<V, E> Path<V, Path<V, E>> {
394    /// Flattens a path of paths into a single path.
395    ///
396    /// Unlike [`flatten_in`](Self::flatten_in), this method does not check that the
397    /// composite is well typed before computing it.
398    pub fn flatten(self) -> Path<V, E> {
399        match self {
400            Path::Id(x) => Path::Id(x),
401            Path::Seq(paths) => {
402                if paths.iter().any(|p| matches!(p, Path::Seq(_))) {
403                    // We either have at least one non-empty sequence...
404                    let edges = paths
405                        .into_iter()
406                        .filter_map(|p| match p {
407                            Path::Id(_) => None,
408                            Path::Seq(edges) => Some(edges),
409                        })
410                        .flatten();
411                    Path::Seq(NonEmpty::collect(edges).unwrap())
412                } else {
413                    // ...or else every path is an identity.
414                    paths.head
415                }
416            }
417        }
418    }
419
420    /// Flattens a path of paths in a graph into a single path.
421    ///
422    /// Returns the flattened path just when the original paths have compatible
423    /// start and end points.
424    pub fn flatten_in(self, graph: &impl Graph<V = V, E = E>) -> Option<Path<V, E>>
425    where
426        V: Eq + Clone,
427    {
428        if let Path::Seq(paths) = &self
429            && !paths.iter().tuple_windows().all(|(p1, p2)| p1.tgt(graph) == p2.src(graph))
430        {
431            None
432        } else {
433            Some(self.flatten())
434        }
435    }
436}
437
438/// A path of length at most one.
439///
440/// We call a path of length at most one, i.e., a path of lenth zero or one, a
441/// short path**. This is not standard terminology, though it is inspired by
442/// "short maps" between metric spaces, which are Lipschitz maps with Lipschitz
443/// constant at most 1.
444///
445/// Short paths are convertible into, and fallibly from, [paths](Path). Short paths
446/// might seem like an odd data structure, but are occasionally useful, such as in:
447///
448/// - *finite* categories defined by an explicit multiplication table, where every
449///   morphism is either a generator (path of length one) or an identity (path of
450///   length zero)
451/// - *augmented* virtual double categories ([Koudenburg
452///   2020](crate::refs::AugmentedVDCs)), where the codomain of a cell is by
453///   definition a short path, and relatedly *unital* virtual double categories
454#[derive(Clone, Debug, PartialEq, Eq, From)]
455pub enum ShortPath<V, E> {
456    /// Path of length zero.
457    Zero(V),
458
459    /// Path of length one.
460    #[from]
461    One(E),
462}
463
464/// A short path in a graph with skeletal vertex and edge sets.
465pub type SkelShortPath = ShortPath<usize, usize>;
466
467impl<V, E> From<ShortPath<V, E>> for Path<V, E> {
468    fn from(path: ShortPath<V, E>) -> Self {
469        match path {
470            ShortPath::Zero(v) => Path::Id(v),
471            ShortPath::One(e) => Path::single(e),
472        }
473    }
474}
475
476impl<V, E> TryFrom<Path<V, E>> for ShortPath<V, E> {
477    type Error = ();
478
479    fn try_from(path: Path<V, E>) -> Result<Self, Self::Error> {
480        match path {
481            Path::Id(v) => Ok(ShortPath::Zero(v)),
482            _ => path.only().map(ShortPath::One).ok_or(()),
483        }
484    }
485}
486
487impl<V, E> ShortPath<V, E> {
488    /// Is the path contained in the given graph?
489    pub fn contained_in(&self, graph: &impl Graph<V = V, E = E>) -> bool {
490        match self {
491            ShortPath::Zero(v) => graph.has_vertex(v),
492            ShortPath::One(e) => graph.has_edge(e),
493        }
494    }
495
496    /// Source of the path in the given graph.
497    pub fn src(&self, graph: &impl Graph<V = V, E = E>) -> V
498    where
499        V: Clone,
500    {
501        match self {
502            ShortPath::Zero(v) => v.clone(),
503            ShortPath::One(e) => graph.src(e),
504        }
505    }
506
507    /// Target of the path in the given graph.
508    pub fn tgt(&self, graph: &impl Graph<V = V, E = E>) -> V
509    where
510        V: Clone,
511    {
512        match self {
513            ShortPath::Zero(v) => v.clone(),
514            ShortPath::One(e) => graph.tgt(e),
515        }
516    }
517
518    /// Converts the short path into an edge in the given *reflexive* graph.
519    pub fn as_edge(self, graph: &impl ReflexiveGraph<V = V, E = E>) -> E {
520        match self {
521            ShortPath::Zero(v) => graph.refl(v),
522            ShortPath::One(e) => e,
523        }
524    }
525
526    /// Maps over the short path.
527    pub fn map<CodV, CodE>(
528        self,
529        fv: impl FnOnce(V) -> CodV,
530        fe: impl FnOnce(E) -> CodE,
531    ) -> ShortPath<CodV, CodE> {
532        match self {
533            ShortPath::Zero(v) => ShortPath::Zero(fv(v)),
534            ShortPath::One(e) => ShortPath::One(fe(e)),
535        }
536    }
537}
538
539/// Assertion of an equation between the composites of two paths in a category.
540#[derive(Clone, Debug, PartialEq, Eq, Constructor)]
541pub struct PathEq<V, E> {
542    /// Left hand side of equation.
543    pub lhs: Path<V, E>,
544
545    /// Right hand side of equation.
546    pub rhs: Path<V, E>,
547}
548
549impl<V, E> PathEq<V, E> {
550    /// Source of the path equation in the given graph.
551    ///
552    /// Panics if the two sides of the path equation have different sources.
553    pub fn src(&self, graph: &impl Graph<V = V, E = E>) -> V
554    where
555        V: Eq + Clone,
556    {
557        let (x, y) = (self.lhs.src(graph), self.rhs.src(graph));
558        assert!(x == y, "Both sides of path equation should have same source");
559        x
560    }
561
562    /// Target of the path equation in the given graph.
563    ///
564    /// Panics if the two sides of the path equation have different targets.
565    pub fn tgt(&self, graph: &impl Graph<V = V, E = E>) -> V
566    where
567        V: Eq + Clone,
568    {
569        let (x, y) = (self.lhs.tgt(graph), self.rhs.tgt(graph));
570        assert!(x == y, "Both sides of path equation should have same target");
571        x
572    }
573
574    /// Validates that the path equation is well defined in the given graph.
575    pub fn validate_in<G>(&self, graph: &G) -> Result<(), NonEmpty<InvalidPathEq>>
576    where
577        V: Eq + Clone,
578        G: Graph<V = V, E = E>,
579    {
580        validate::wrap_errors(self.iter_invalid_in(graph))
581    }
582
583    /// Iterators over failures of the path equation to be well defined.
584    pub fn iter_invalid_in<G>(
585        &self,
586        graph: &G,
587    ) -> impl Iterator<Item = InvalidPathEq> + use<G, V, E>
588    where
589        V: Eq + Clone,
590        G: Graph<V = V, E = E>,
591    {
592        let mut errs = Vec::new();
593        if !self.lhs.contained_in(graph) {
594            errs.push(InvalidPathEq::Lhs);
595        }
596        if !self.rhs.contained_in(graph) {
597            errs.push(InvalidPathEq::Rhs);
598        }
599        if errs.is_empty() {
600            if self.lhs.src(graph) != self.rhs.src(graph) {
601                errs.push(InvalidPathEq::Src);
602            }
603            if self.lhs.tgt(graph) != self.rhs.tgt(graph) {
604                errs.push(InvalidPathEq::Tgt);
605            }
606        }
607        errs.into_iter()
608    }
609}
610
611/// A failure of a path equation to be well defined in a graph.
612#[derive(Clone, Debug, PartialEq, Eq)]
613#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
614#[cfg_attr(feature = "serde-wasm", derive(Tsify))]
615#[cfg_attr(feature = "serde-wasm", tsify(into_wasm_abi, from_wasm_abi))]
616pub enum InvalidPathEq {
617    /// Left-hand side of equation is not a valid path in the graph.
618    Lhs,
619
620    /// Right-hand side of equation is not a valid path in the graph.
621    Rhs,
622
623    /// Sources of left- and right-hand sides of path equation are not equal.
624    Src,
625
626    /// Targets of left- and right-hand sides of path equation are not equal.
627    Tgt,
628}
629
630#[cfg(test)]
631mod tests {
632    use super::super::graph::SkelGraph;
633    use super::*;
634    use std::convert::identity;
635
636    #[test]
637    fn path_in_graph() {
638        let g = SkelGraph::triangle();
639        let path = Path::pair(0, 1);
640        assert_eq!(path.src(&g), 0);
641        assert_eq!(path.tgt(&g), 2);
642        assert_eq!(Path::single(0).concat_in(&g, Path::single(1)), Some(path));
643
644        assert!(Path::Id(2).contained_in(&g));
645        assert!(!Path::Id(3).contained_in(&g));
646        assert!(Path::pair(0, 1).contained_in(&g));
647        assert!(!Path::pair(1, 0).contained_in(&g));
648    }
649
650    #[test]
651    fn short_paths() {
652        let (v, e) = (1, 1);
653        let path: SkelPath = ShortPath::Zero(v).into();
654        assert_eq!(path.try_into(), Ok(SkelShortPath::Zero(v)));
655        let path: SkelPath = ShortPath::One(e).into();
656        assert_eq!(path.clone().only(), Some(e));
657        assert_eq!(path.try_into(), Ok(ShortPath::One(e)));
658    }
659
660    #[test]
661    fn insert_into_path() {
662        let mut path = SkelPath::Id(0);
663        path.insert(0, 2);
664        assert_eq!(path, Path::single(2));
665        path.insert(0, 1);
666        assert_eq!(path, Path::pair(1, 2));
667
668        assert_eq!(SkelPath::empty(0).splice(0..0, Path::pair(0, 1)), Path::pair(0, 1));
669        assert_eq!(SkelPath::empty(0).splice(0..0, Path::empty(0)), Path::empty(0));
670        let target = SkelPath::Seq(nonempty![0, 1, 2]);
671        assert_eq!(Path::pair(0, 2).splice(1..1, Path::single(1)), target);
672        assert_eq!(Path::pair(0, 2).splice(1..2, Path::pair(1, 2)), target);
673        assert_eq!(target.clone().splice(1..3, Path::pair(1, 2)), target);
674        assert_eq!(target.clone().splice(1..1, Path::empty(0)), target);
675    }
676
677    #[test]
678    fn subpath() {
679        let g = SkelGraph::path(4);
680        assert_eq!(Path::Id(1).subpath(&g, 0..0), Path::Id(1));
681        let path = Path::Seq(nonempty![0, 1, 2]);
682        assert_eq!(path.subpath(&g, 0..0), Path::Id(0));
683        assert_eq!(path.subpath(&g, 1..1), Path::Id(1));
684        assert_eq!(path.subpath(&g, 3..3), Path::Id(3));
685        assert_eq!(path.subpath(&g, 0..2), Path::pair(0, 1));
686        assert_eq!(path.subpath(&g, 1..3), Path::pair(1, 2));
687    }
688
689    #[test]
690    fn map_path() {
691        let id = SkelPath::Id(1);
692        assert_eq!(id.iter().count(), 0);
693        assert_eq!(id.clone().into_iter().count(), 0);
694        assert_eq!(id.clone().map(|v| v + 1, identity), Path::Id(2));
695        assert_eq!(id.partial_map(|v| Some(v + 1), Some), Some(Path::Id(2)));
696
697        let pair = SkelPath::pair(0, 1);
698        assert_eq!(pair.iter().count(), 2);
699        assert_eq!(pair.clone().into_iter().count(), 2);
700        assert_eq!(pair.clone().map(identity, |e| e + 1), Path::pair(1, 2));
701        assert_eq!(pair.partial_map(Some, |e| Some(e + 1)), Some(Path::pair(1, 2)));
702    }
703
704    #[test]
705    fn path_eq() {
706        let g = SkelGraph::triangle();
707        let eq = PathEq::new(Path::pair(0, 1), Path::single(2));
708        assert_eq!(eq.src(&g), 0);
709        assert_eq!(eq.tgt(&g), 2);
710        assert!(eq.validate_in(&g).is_ok());
711    }
712
713    #[test]
714    fn path_is_simple() {
715        assert!(SkelPath::pair(0, 1).is_simple());
716        assert!(!SkelPath::pair(0, 0).is_simple());
717        assert!(SkelPath::Id(0).is_simple());
718    }
719}