harness_space/
lattice.rs

1//! # Lattice Module
2//! Defines a generic lattice data structure and associated operations.
3//! This module provides `Lattice<T>`, a structure capable of representing
4//! any partially ordered set where `T` is the type of elements in the lattice.
5//!
6//! ## Features
7//! - Creation of lattices and addition of elements and relations (`a ≤ b`).
8//! - Automatic computation of transitive closure to represent all implied relations.
9//! - Checking for relations (`leq`).
10//! - Finding minimal and maximal elements.
11//! - Computation of join (least upper bound) and meet (greatest lower bound) of elements.
12//! - Exporting the lattice structure to a DOT file format for visualization (e.g., with Graphviz).
13//!
14//! ## Type Constraints
15//! - The element type `T` must implement `std::hash::Hash`, `std::cmp::Eq`, and `std::clone::Clone`
16//!   for basic lattice operations.
17//! - For generating DOT file output using `save_to_dot_file`, `T` must also implement
18//!   `std::fmt::Display` (for node labels) and `std::cmp::Ord` (for consistent output ordering).
19//!
20//! ## Example Usage
21//! ```
22//! use harness_space::{lattice::Lattice, prelude::*};
23//!
24//! // Create a new lattice for integers
25//! let mut lattice: Lattice<i32> = Lattice::new();
26//!
27//! // Add elements and relations (e.g., a simple chain 1 ≤ 2 ≤ 3)
28//! lattice.add_relation(1, 2);
29//! lattice.add_relation(2, 3);
30//!
31//! // Check relations
32//! assert!(lattice.leq(&1, &3).unwrap());
33//! assert!(!lattice.leq(&3, &1).unwrap());
34//!
35//! // Find minimal and maximal elements
36//! let minimal = lattice.minimal_elements();
37//! assert!(minimal.contains(&1) && minimal.len() == 1);
38//! let maximal = lattice.maximal_elements();
39//! assert!(maximal.contains(&3) && maximal.len() == 1);
40//!
41//! // Compute join and meet (for a diamond lattice, for example)
42//! let mut diamond: Lattice<char> = Lattice::new();
43//! diamond.add_relation('d', 'b'); // d is bottom
44//! diamond.add_relation('d', 'c');
45//! diamond.add_relation('b', 'a'); // a is top
46//! diamond.add_relation('c', 'a');
47//!
48//! assert_eq!(diamond.join('b', 'c'), Some('a'));
49//! assert_eq!(diamond.meet('b', 'c'), Some('d'));
50//!
51//! // Save to a DOT file (requires T: Display + Ord)
52//! // if let Err(e) = diamond.save_to_dot_file("diamond_lattice.dot") {
53//! //     eprintln!("Failed to save: {}", e);
54//! // }
55//! ```
56
57use std::{
58  collections::{HashMap, HashSet},
59  fs::File,
60  hash::Hash,
61  io::{Result as IoResult, Write as IoWrite},
62};
63
64use crate::set::{Collection, Poset};
65
66/// A node in a lattice representing an element and its relationships.
67///
68/// Each node stores an element of type `T` and maintains sets of its direct
69/// successors (elements greater than this one) and direct predecessors
70/// (elements less than this one) in the partial order.
71#[derive(Debug, Clone)]
72pub struct LatticeNode<T> {
73  /// The element stored in this node.
74  element:      T,
75  /// Direct successors (elements that are greater than this one according to the
76  /// lattice's partial order).
77  successors:   HashSet<T>,
78  /// Direct predecessors (elements that are less than this one according to the
79  /// lattice's partial order).
80  predecessors: HashSet<T>,
81}
82
83/// A general lattice structure that can represent any partially ordered set
84/// with join and meet operations.
85///
86/// A lattice is a partially ordered set in which any two elements have a unique
87/// supremum (also called a least upper bound or join) and a unique infimum
88/// (also called a greatest lower bound or meet).
89/// This implementation stores elements of type `T` and their relationships.
90/// The type `T` must implement `Hash`, `Eq`, and `Clone`.
91/// For DOT file generation, `T` must also implement `Display` and `Ord`.
92#[derive(Debug, Default, Clone)]
93pub struct Lattice<T> {
94  /// Map of elements to their nodes. Each key is an element in the lattice,
95  /// and its value is the `LatticeNode` containing the element's relationships.
96  nodes: HashMap<T, LatticeNode<T>>,
97}
98
99impl<T: Hash + Eq + Clone> Lattice<T> {
100  /// Creates a new empty lattice.
101  ///
102  /// # Examples
103  ///
104  /// ```
105  /// use harness_space::{lattice::Lattice, prelude::*};
106  /// let lattice: Lattice<i32> = Lattice::new();
107  /// ```
108  pub fn new() -> Self { Self { nodes: HashMap::new() } }
109
110  /// Adds a new element to the lattice.
111  ///
112  /// If the element already exists in the lattice, this method does nothing.
113  ///
114  /// # Arguments
115  ///
116  /// * `element`: The element to add to the lattice.
117  ///
118  /// # Examples
119  ///
120  /// ```
121  /// use harness_space::{lattice::Lattice, prelude::*};
122  /// let mut lattice = Lattice::new();
123  /// lattice.add_element(1);
124  /// ```
125  pub fn add_element(&mut self, element: T) {
126    if !self.nodes.contains_key(&element) {
127      self.nodes.insert(element.clone(), LatticeNode {
128        element,
129        successors: HashSet::new(),
130        predecessors: HashSet::new(),
131      });
132    }
133  }
134
135  /// Adds a relation `a ≤ b` to the lattice.
136  ///
137  /// This indicates that element `a` is less than or equal to element `b`
138  /// in the partial order. If `a` or `b` are not already in the lattice,
139  /// they are added.
140  ///
141  /// After adding the direct relation, this method also updates the transitive
142  /// closure of the lattice to ensure all indirect relationships are captured.
143  ///
144  /// # Arguments
145  ///
146  /// * `a`: The smaller element in the relation.
147  /// * `b`: The greater element in the relation.
148  ///
149  /// # Examples
150  ///
151  /// ```
152  /// use harness_space::{lattice::Lattice, prelude::*};
153  /// let mut lattice = Lattice::new();
154  /// lattice.add_relation(1, 2); // 1 ≤ 2
155  /// ```
156  pub fn add_relation(&mut self, a: T, b: T) {
157    self.add_element(a.clone());
158    self.add_element(b.clone());
159
160    // Add direct relation
161    if let Some(node_a) = self.nodes.get_mut(&a) {
162      node_a.successors.insert(b.clone());
163    }
164    if let Some(node_b) = self.nodes.get_mut(&b) {
165      node_b.predecessors.insert(a);
166    }
167
168    // Transitive closure
169    self.compute_transitive_closure();
170  }
171
172  /// Computes the transitive closure of the lattice
173  fn compute_transitive_closure(&mut self) {
174    let mut changed = true;
175    while changed {
176      changed = false;
177      let mut updates = Vec::new();
178
179      // Collect all updates first
180      for node in self.nodes.values() {
181        for succ in &node.successors {
182          if let Some(succ_node) = self.nodes.get(succ) {
183            for succ_succ in &succ_node.successors {
184              updates.push((node.element.clone(), succ_succ.clone()));
185            }
186          }
187        }
188      }
189
190      // Apply updates
191      for (a, b) in updates {
192        if let Some(node_a) = self.nodes.get_mut(&a) {
193          if node_a.successors.insert(b.clone()) {
194            changed = true;
195          }
196        }
197        if let Some(node_b) = self.nodes.get_mut(&b) {
198          if node_b.predecessors.insert(a) {
199            changed = true;
200          }
201        }
202      }
203    }
204  }
205}
206
207impl<T: Hash + Eq + Clone> Collection for Lattice<T> {
208  type Item = T;
209
210  fn contains(&self, point: &Self::Item) -> bool { self.nodes.contains_key(point) }
211
212  fn is_empty(&self) -> bool { self.nodes.is_empty() }
213}
214
215impl<T: Hash + Eq + Clone> Poset for Lattice<T> {
216  /// Checks if `a ≤ b` in the lattice.
217  ///
218  /// This method determines if element `a` is less than or equal to element `b`
219  /// according to the partial order defined in the lattice. This relies on the
220  /// precomputed transitive closure.
221  ///
222  /// # Arguments
223  ///
224  /// * `a`: The first element.
225  /// * `b`: The second element.
226  ///
227  /// # Returns
228  ///
229  /// `true` if `a ≤ b`, `false` otherwise. Returns `false` if either `a` or `b`
230  /// is not in the lattice.
231  ///
232  /// # Examples
233  ///
234  /// ```
235  /// use harness_space::{lattice::Lattice, prelude::*};
236  /// let mut lattice = Lattice::new();
237  /// lattice.add_relation(1, 2);
238  /// lattice.add_relation(2, 3);
239  /// assert!(lattice.leq(&1, &3).unwrap()); // Transitive: 1 ≤ 2 and 2 ≤ 3 => 1 ≤ 3
240  /// assert!(!lattice.leq(&3, &1).unwrap());
241  /// ```
242  fn leq(&self, a: &T, b: &T) -> Option<bool> {
243    if !self.nodes.contains_key(a) || !self.nodes.contains_key(b) {
244      return None;
245    }
246    if a == b {
247      return Some(true);
248    }
249    let node_a = self.nodes.get(a).unwrap();
250    Some(node_a.successors.contains(b))
251  }
252
253  /// Returns all minimal elements in the lattice.
254  ///
255  /// Minimal elements are those that have no predecessors in the partial order.
256  ///
257  /// # Returns
258  ///
259  /// A `HashSet` containing all minimal elements of the lattice.
260  /// If the lattice is empty, an empty set is returned.
261  ///
262  /// # Examples
263  ///
264  /// ```
265  /// use harness_space::{lattice::Lattice, prelude::*};
266  /// let mut lattice = Lattice::new();
267  /// lattice.add_relation(1, 2);
268  /// lattice.add_relation(1, 3);
269  /// let minimal = lattice.minimal_elements();
270  /// assert!(minimal.contains(&1) && minimal.len() == 1);
271  /// ```
272  fn minimal_elements(&self) -> HashSet<T> {
273    self
274      .nodes
275      .iter()
276      .filter(|(_, node)| node.predecessors.is_empty())
277      .map(|(element, _)| element.clone())
278      .collect()
279  }
280
281  /// Returns all maximal elements in the lattice.
282  ///
283  /// Maximal elements are those that have no successors in the partial order.
284  ///
285  /// # Returns
286  ///
287  /// A `HashSet` containing all maximal elements of the lattice.
288  /// If the lattice is empty, an empty set is returned.
289  ///
290  /// # Examples
291  ///
292  /// ```
293  /// use harness_space::{lattice::Lattice, prelude::*};
294  /// let mut lattice = Lattice::new();
295  /// lattice.add_relation(1, 3);
296  /// lattice.add_relation(2, 3);
297  /// let maximal = lattice.maximal_elements();
298  /// assert!(maximal.contains(&3) && maximal.len() == 1);
299  /// ```
300  fn maximal_elements(&self) -> HashSet<T> {
301    self
302      .nodes
303      .iter()
304      .filter(|(_, node)| node.successors.is_empty())
305      .map(|(element, _)| element.clone())
306      .collect()
307  }
308
309  /// Computes the join (least upper bound) of two elements `a` and `b`.
310  ///
311  /// The join of `a` and `b` is an element `x` such that `a ≤ x` and `b ≤ x`,
312  /// and for any other element `y` with `a ≤ y` and `b ≤ y`, it holds that `x ≤ y`.
313  ///
314  /// # Arguments
315  ///
316  /// * `a`: The first element.
317  /// * `b`: The second element.
318  ///
319  /// # Returns
320  ///
321  /// An `Option<T>` containing the unique join of `a` and `b` if it exists.
322  /// Returns `None` if:
323  /// * Either `a` or `b` is not in the lattice.
324  /// * `a` and `b` have no common upper bounds.
325  /// * `a` and `b` have multiple minimal common upper bounds (i.e., the join is not unique).
326  ///
327  /// # Examples
328  ///
329  /// ```
330  /// use harness_space::{lattice::Lattice, prelude::*};
331  /// let mut lattice = Lattice::new(); // Diamond lattice
332  /// lattice.add_relation(4, 2);
333  /// lattice.add_relation(4, 3);
334  /// lattice.add_relation(2, 1);
335  /// lattice.add_relation(3, 1);
336  /// // Here, 4 is bottom, 1 is top.
337  /// assert_eq!(lattice.join(2, 3), Some(1));
338  /// assert_eq!(lattice.join(4, 2), Some(2));
339  /// ```
340  fn join(&self, a: T, b: T) -> Option<T> {
341    if !self.nodes.contains_key(&a) || !self.nodes.contains_key(&b) {
342      return None; // Elements must be in the lattice
343    }
344
345    let node_a = self.nodes.get(&a).unwrap();
346    let node_b = self.nodes.get(&b).unwrap();
347
348    let mut upper_bounds_a = node_a.successors.iter().cloned().collect::<HashSet<T>>();
349    upper_bounds_a.insert(a.clone());
350
351    let mut upper_bounds_b = node_b.successors.iter().cloned().collect::<HashSet<T>>();
352    upper_bounds_b.insert(b.clone());
353
354    let common_upper_bounds: HashSet<T> =
355      upper_bounds_a.intersection(&upper_bounds_b).cloned().collect();
356
357    if common_upper_bounds.is_empty() {
358      return None;
359    }
360
361    let minimal_common_upper_bounds: Vec<T> = common_upper_bounds
362      .iter()
363      .filter(|&x| common_upper_bounds.iter().all(|y| x == y || !self.leq(y, x).unwrap_or(false)))
364      .cloned()
365      .collect();
366
367    if minimal_common_upper_bounds.len() == 1 {
368      Some(minimal_common_upper_bounds[0].clone())
369    } else {
370      None
371    }
372  }
373
374  /// Computes the meet (greatest lower bound) of two elements `a` and `b`.
375  ///
376  /// The meet of `a` and `b` is an element `x` such that `x ≤ a` and `x ≤ b`,
377  /// and for any other element `y` with `y ≤ a` and `y ≤ b`, it holds that `y ≤ x`.
378  ///
379  /// # Arguments
380  ///
381  /// * `a`: The first element.
382  /// * `b`: The second element.
383  ///
384  /// # Returns
385  ///
386  /// An `Option<T>` containing the unique meet of `a` and `b` if it exists.
387  /// Returns `None` if:
388  /// * Either `a` or `b` is not in the lattice.
389  /// * `a` and `b` have no common lower bounds.
390  /// * `a` and `b` have multiple maximal common lower bounds (i.e., the meet is not unique).
391  ///
392  /// # Examples
393  ///
394  /// ```
395  /// use harness_space::{lattice::Lattice, prelude::*};
396  /// let mut lattice = Lattice::new(); // Diamond lattice
397  /// lattice.add_relation(4, 2);
398  /// lattice.add_relation(4, 3);
399  /// lattice.add_relation(2, 1);
400  /// lattice.add_relation(3, 1);
401  /// // Here, 4 is bottom, 1 is top.
402  /// assert_eq!(lattice.meet(2, 3), Some(4));
403  /// assert_eq!(lattice.meet(1, 2), Some(2));
404  /// ```
405  fn meet(&self, a: T, b: T) -> Option<T> {
406    if !self.nodes.contains_key(&a) || !self.nodes.contains_key(&b) {
407      return None; // Elements must be in the lattice
408    }
409
410    let node_a = self.nodes.get(&a).unwrap();
411    let node_b = self.nodes.get(&b).unwrap();
412
413    let mut lower_bounds_a = node_a.predecessors.iter().cloned().collect::<HashSet<T>>();
414    lower_bounds_a.insert(a.clone());
415
416    let mut lower_bounds_b = node_b.predecessors.iter().cloned().collect::<HashSet<T>>();
417    lower_bounds_b.insert(b.clone());
418
419    let common_lower_bounds: HashSet<T> =
420      lower_bounds_a.intersection(&lower_bounds_b).cloned().collect();
421
422    if common_lower_bounds.is_empty() {
423      return None;
424    }
425
426    let maximal_common_lower_bounds: Vec<T> = common_lower_bounds
427      .iter()
428      .filter(|&x| common_lower_bounds.iter().all(|y| x == y || !self.leq(x, y).unwrap_or(false)))
429      .cloned()
430      .collect();
431
432    if maximal_common_lower_bounds.len() == 1 {
433      Some(maximal_common_lower_bounds[0].clone())
434    } else {
435      None
436    }
437  }
438
439  /// Returns the set of elements that are less than or equal to `a`.
440  ///
441  /// This method returns a `HashSet` containing all elements in the lattice
442  /// that are less than or equal to `a`. If `a` is not in the lattice,
443  /// the method returns an empty set.
444  ///
445  /// # Arguments
446  ///
447  /// * `a`: The element to get the lower bounds of.
448  ///
449  /// # Returns
450  ///
451  /// A `HashSet` containing all elements in the lattice that are less than or equal to `a`.
452  fn downset(&self, a: T) -> HashSet<T> {
453    self
454      .nodes
455      .iter()
456      .filter(|(_, node)| self.leq(&node.element, &a).unwrap_or(false))
457      .map(|(element, _)| element.clone())
458      .collect()
459  }
460
461  /// Returns the set of elements that are greater than or equal to `a`.
462  ///
463  /// This method returns a `HashSet` containing all elements in the lattice
464  /// that are greater than or equal to `a`. If `a` is not in the lattice,
465  /// the method returns an empty set.
466  ///
467  /// # Arguments
468  ///
469  /// * `a`: The element to get the upper bounds of.
470  ///
471  /// # Returns
472  ///
473  /// A `HashSet` containing all elements in the lattice that are greater than or equal to `a`.
474  fn upset(&self, a: T) -> HashSet<T> {
475    self
476      .nodes
477      .iter()
478      .filter(|(_, node)| self.leq(&a, &node.element).unwrap_or(false))
479      .map(|(element, _)| element.clone())
480      .collect()
481  }
482
483  /// Returns the set of elements that are successors of `a`.
484  ///
485  /// This method returns a `HashSet` containing all elements in the lattice
486  /// that are direct successors of `a`. If `a` is not in the lattice,
487  /// the method returns an empty set.
488  fn successors(&self, a: T) -> HashSet<T> {
489    self.nodes.get(&a).map_or_else(HashSet::new, |node_a| {
490      // Filter to only include direct successors
491      let all_successors = &node_a.successors;
492      all_successors
493        .iter()
494        .filter(|&b| {
495          // A successor b is direct if there's no other element c where a < c < b
496          !all_successors.iter().any(|c| {
497            c != b && self.nodes.get(c).is_some_and(|node_c| node_c.successors.contains(b))
498          })
499        })
500        .cloned()
501        .collect()
502    })
503  }
504
505  /// Returns the set of elements that are predecessors of `a`.
506  ///
507  /// This method returns a `HashSet` containing all elements in the lattice
508  /// that are direct predecessors of `a`. If `a` is not in the lattice,
509  /// the method returns an empty set.
510  fn predecessors(&self, a: T) -> HashSet<T> {
511    self.nodes.get(&a).map_or_else(HashSet::new, |node_a| {
512      // Filter to only include direct predecessors
513      let all_predecessors = &node_a.predecessors;
514      all_predecessors
515        .iter()
516        .filter(|&b| {
517          // A predecessor b is direct if there's no other element c where b < c < a
518          !all_predecessors.iter().any(|c| {
519            c != b && self.nodes.get(c).is_some_and(|node_c| node_c.predecessors.contains(b))
520          })
521        })
522        .cloned()
523        .collect()
524    })
525  }
526}
527
528// Helper function to escape strings for DOT format
529fn escape_dot_label(label: &str) -> String { label.replace('"', "\\\"") }
530
531// Implementation block for methods requiring Display and Ord for T
532impl<T: Hash + Eq + Clone + std::fmt::Display + Ord> Lattice<T> {
533  /// Saves the lattice representation in DOT format to the specified file.
534  ///
535  /// This method generates a string in the DOT language (used by Graphviz)
536  /// representing the Hasse diagram of the lattice and writes it to the given file.
537  /// The diagram shows only the covering relations (immediate successor/predecessor).
538  /// Elements are displayed using their `Display` implementation.
539  /// The layout is bottom-to-top (`rankdir="BT"`).
540  ///
541  /// Requires `T` to implement `std::fmt::Display` and `std::cmp::Ord`
542  /// for consistent node labeling and ordering in the output.
543  ///
544  /// # Arguments
545  ///
546  /// * `filename`: The path to the file where the DOT representation will be saved. If the file
547  ///   exists, it will be overwritten.
548  ///
549  /// # Returns
550  ///
551  /// An `IoResult<()>` which is `Ok(())` on successful write, or an `Err`
552  /// containing an `std::io::Error` if any I/O error occurs (e.g., file
553  /// creation fails).
554  ///
555  /// # Panics
556  ///
557  /// This method does not explicitly panic, but file operations can panic
558  /// under certain unrecoverable conditions (though `File::create` and `writeln!`
559  /// typically return `Result`).
560  ///
561  /// # Examples
562  ///
563  /// ```no_run
564  /// use harness_space::lattice::Lattice;
565  /// let mut lattice = Lattice::new();
566  /// lattice.add_relation("a", "b");
567  /// lattice.add_relation("b", "c");
568  /// if let Err(e) = lattice.save_to_dot_file("my_lattice.dot") {
569  ///   eprintln!("Failed to save lattice: {}", e);
570  /// }
571  /// ```
572  ///
573  /// The resulting `my_lattice.dot` file would look something like:
574  /// ```dot
575  /// digraph Lattice {
576  ///   rankdir="BT";
577  ///   node [shape=plaintext];
578  ///   "a";
579  ///   "b";
580  ///   "c";
581  ///
582  ///   "a" -> "b";
583  ///   "b" -> "c";
584  /// }
585  /// ```
586  pub fn save_to_dot_file(&self, filename: &str) -> IoResult<()> {
587    let mut file = File::create(filename)?;
588
589    if self.nodes.is_empty() {
590      return writeln!(file, "digraph Lattice {{\n  label=\"Empty Lattice\";\n}}");
591    }
592
593    writeln!(file, "digraph Lattice {{")?;
594    writeln!(file, "  rankdir=\"BT\";")?;
595    writeln!(file, "  node [shape=plaintext];")?;
596
597    let mut sorted_node_keys: Vec<&T> = self.nodes.keys().collect();
598    // T: Ord is required for sorted_node_keys.sort()
599    // &T will be sorted based on the Ord impl of T.
600    sorted_node_keys.sort();
601
602    // Define nodes
603    for node_key_ptr in &sorted_node_keys {
604      let node_key = *node_key_ptr; // node_key is &T
605                                    // node_key.to_string() requires T: std::fmt::Display
606      writeln!(file, "  \"{}\";", escape_dot_label(&node_key.to_string()))?;
607    }
608    writeln!(file)?; // Blank line for readability
609
610    // Define edges (covering relations)
611    for source_key_ptr in &sorted_node_keys {
612      let source_key = *source_key_ptr; // source_key is &T
613      if let Some(node) = self.nodes.get(source_key) {
614        let mut sorted_successors: Vec<&T> = node.successors.iter().collect();
615        sorted_successors.sort(); // T: Ord required for &T to sort
616
617        for succ_key in sorted_successors {
618          // succ_key is &T
619          let mut is_immediate = true;
620          let mut inner_sorted_successors_for_check: Vec<&T> = node.successors.iter().collect();
621          inner_sorted_successors_for_check.sort(); // T: Ord required
622
623          for intermediate_key in inner_sorted_successors_for_check {
624            if intermediate_key == succ_key {
625              continue;
626            }
627            if let Some(intermediate_node_w) = self.nodes.get(intermediate_key) {
628              if intermediate_node_w.successors.contains(succ_key) {
629                is_immediate = false;
630                break;
631              }
632            }
633          }
634
635          if is_immediate {
636            writeln!(
637              file,
638              "  \"{}\" -> \"{}\";",
639              escape_dot_label(&source_key.to_string()), // T: Display
640              escape_dot_label(&succ_key.to_string())    // T: Display
641            )?;
642          }
643        }
644      }
645    }
646    writeln!(file, "}}")
647  }
648}
649
650#[cfg(test)]
651mod tests {
652  use super::*;
653
654  fn m_lattice() -> Lattice<i32> {
655    let mut m_lattice: Lattice<i32> = Lattice::new();
656    // M-shape for non-unique meet if 5,6 considered uppers for join(1,3)
657    //   5   6
658    //  / \ / \
659    // 1   2   3
660    // join(1,3) -> should be None if 5 and 6 are incomparable & minimal uppers
661    m_lattice.add_element(1);
662    m_lattice.add_element(2);
663    m_lattice.add_element(3);
664    m_lattice.add_element(5);
665    m_lattice.add_element(6);
666
667    m_lattice.add_relation(1, 5);
668    m_lattice.add_relation(2, 5);
669    m_lattice.add_relation(2, 6);
670    m_lattice.add_relation(3, 6);
671    m_lattice
672  }
673
674  fn diamond_lattice() -> Lattice<i32> {
675    // Create a diamond lattice:
676    //     1
677    //    / \
678    //   2   3
679    //    \ /
680    //     4
681    let mut diamond_lattice: Lattice<i32> = Lattice::new();
682    diamond_lattice.add_element(1);
683    diamond_lattice.add_element(2);
684    diamond_lattice.add_element(3);
685    diamond_lattice.add_element(4);
686
687    diamond_lattice.add_relation(1, 2);
688    diamond_lattice.add_relation(1, 3);
689    diamond_lattice.add_relation(2, 4);
690    diamond_lattice.add_relation(3, 4);
691
692    diamond_lattice
693  }
694
695  #[test]
696  fn test_basic_lattice() {
697    let mut lattice = Lattice::new();
698    lattice.add_relation(1, 2);
699    lattice.add_relation(2, 3);
700
701    assert!(lattice.leq(&1, &2).unwrap_or(false));
702    assert!(lattice.leq(&2, &3).unwrap_or(false));
703    assert!(lattice.leq(&1, &3).unwrap_or(false));
704    assert!(!lattice.leq(&2, &1).unwrap_or(false));
705
706    let minimal = lattice.minimal_elements();
707    assert_eq!(minimal.len(), 1);
708    assert!(minimal.contains(&1));
709
710    let maximal = lattice.maximal_elements();
711    assert_eq!(maximal.len(), 1);
712    assert!(maximal.contains(&3));
713  }
714
715  #[test]
716  fn test_diamond_lattice() {
717    let lattice = diamond_lattice();
718    assert!(lattice.leq(&1, &4).unwrap_or(false));
719    assert!(!lattice.leq(&2, &3).unwrap_or(false));
720    assert!(!lattice.leq(&3, &2).unwrap_or(false));
721
722    let minimal = lattice.minimal_elements();
723    assert_eq!(minimal.len(), 1);
724    assert!(minimal.contains(&1));
725
726    let maximal = lattice.maximal_elements();
727    assert_eq!(maximal.len(), 1);
728    assert!(maximal.contains(&4));
729  }
730
731  #[test]
732  fn test_lattice_operations_diamond() {
733    let mut lattice = Lattice::new();
734    lattice.add_relation(1, 2);
735    lattice.add_relation(1, 3);
736    lattice.add_relation(2, 4);
737    lattice.add_relation(3, 4);
738
739    // Test join
740    println!("join(2, 3): {:?}", lattice.join(2, 3));
741    assert_eq!(lattice.join(2, 3), Some(4));
742
743    println!("join(1, 4): {:?}", lattice.join(1, 4));
744    assert_eq!(lattice.join(1, 4), Some(4));
745
746    println!("join(1, 2): {:?}", lattice.join(1, 2));
747    assert_eq!(lattice.join(1, 2), Some(2));
748
749    println!("join(1, 1): {:?}", lattice.join(1, 1));
750    assert_eq!(lattice.join(1, 1), Some(1));
751
752    // Test meet
753    println!("meet(2, 3): {:?}", lattice.meet(2, 3));
754    assert_eq!(lattice.meet(2, 3), Some(1));
755
756    println!("meet(1, 4): {:?}", lattice.meet(1, 4));
757    assert_eq!(lattice.meet(1, 4), Some(1));
758
759    println!("meet(2, 4): {:?}", lattice.meet(2, 4));
760    assert_eq!(lattice.meet(2, 4), Some(2));
761
762    println!("meet(4, 4): {:?}", lattice.meet(4, 4));
763    assert_eq!(lattice.meet(4, 4), Some(4));
764  }
765
766  #[test]
767  fn test_lattice_operations_non_lattice_examples() {
768    let m_lattice = m_lattice();
769
770    println!("join(1, 2) for M-shape: {:?}", m_lattice.join(1, 2));
771    assert_eq!(m_lattice.join(1, 2), Some(5));
772
773    println!("join(2, 3) for M-shape: {:?}", m_lattice.join(2, 3));
774    assert_eq!(m_lattice.join(2, 3), Some(6));
775
776    println!("join(1, 3) for M-shape: {:?}", m_lattice.join(1, 3));
777    assert_eq!(m_lattice.join(1, 3), None);
778
779    // Example with two minimal upper bounds:
780    //   c   d
781    //  / \ /
782    // a   b
783    let mut non_join_lattice: Lattice<&str> = Lattice::new();
784    non_join_lattice.add_relation("a", "c");
785    non_join_lattice.add_relation("a", "d");
786    non_join_lattice.add_relation("b", "c");
787    non_join_lattice.add_relation("b", "d");
788
789    assert_eq!(non_join_lattice.join("a", "b"), None);
790    assert_eq!(non_join_lattice.meet("c", "d"), None);
791
792    // Example with two maximal lower bounds:
793    //   a   b
794    //  / \ / \
795    // c   d
796    let mut non_meet_lattice: Lattice<&str> = Lattice::new();
797    non_meet_lattice.add_relation("c", "a");
798    non_meet_lattice.add_relation("d", "a");
799    non_meet_lattice.add_relation("c", "b");
800    non_meet_lattice.add_relation("d", "b");
801
802    assert_eq!(non_meet_lattice.meet("a", "b"), None);
803    assert_eq!(non_meet_lattice.join("c", "d"), None);
804  }
805
806  #[test]
807  fn test_graphviz_output() {
808    let temp_dir = tempfile::tempdir().unwrap();
809    let temp_path = temp_dir.path().join("test_m_shape_lattice.dot");
810    let filename = temp_path.to_str().unwrap();
811    println!("--- M-shape Example - Saving to {filename} ---");
812    m_lattice().save_to_dot_file(filename).expect("Failed to save M-shape lattice");
813
814    // Check if the file was created
815    assert!(temp_path.exists());
816
817    // Clean up the temporary directory
818    drop(temp_dir);
819  }
820
821  #[test]
822  #[ignore = "Manual test to see output of M-shape lattice"]
823  fn test_graphviz_output_manual() {
824    let filename = "test_m_shape_lattice.dot";
825    println!("--- M-shape Example - Saving to {filename} ---");
826    m_lattice().save_to_dot_file(filename).expect("Failed to save M-shape lattice");
827  }
828
829  #[test]
830  fn test_downset() {
831    let lattice = diamond_lattice();
832    let downset = lattice.downset(4);
833    assert_eq!(downset, HashSet::from([1, 2, 3, 4]));
834
835    let downset = lattice.downset(2);
836    assert_eq!(downset, HashSet::from([1, 2]));
837
838    let downset = lattice.downset(1);
839    assert_eq!(downset, HashSet::from([1]));
840  }
841
842  #[test]
843  fn test_upset() {
844    let lattice = diamond_lattice();
845    let upset = lattice.upset(4);
846    assert_eq!(upset, HashSet::from([4]));
847
848    let upset = lattice.upset(2);
849    assert_eq!(upset, HashSet::from([2, 4]));
850
851    let upset = lattice.upset(1);
852    assert_eq!(upset, HashSet::from([1, 2, 3, 4]));
853  }
854
855  #[test]
856  fn test_successors() {
857    let lattice = diamond_lattice();
858    let successors = lattice.successors(1);
859    assert_eq!(successors, HashSet::from([2, 3]));
860  }
861
862  #[test]
863  fn test_predecessors() {
864    let lattice = diamond_lattice();
865    let predecessors = lattice.predecessors(4);
866    assert_eq!(predecessors, HashSet::from([2, 3]));
867  }
868}