gpuequiv/lib.rs
1//! A GPU-accelerated implementation of an algorithm to find all equivalences
2//! of processes in the Linear-time Branching-time Spectrum.
3//!
4//! This project is part of my bachelor's thesis at Technische Universität Berlin.
5//! The [accompanying thesis text](https://github.com/Gobbel2000/thesis-gpuequiv)
6//! can be consulted as a more in-depth documentation.
7//!
8//! This is a Rust crate implementing the
9//! [Spectroscopy algorithm](https://arxiv.org/abs/2303.08904) by B. Bisping
10//! with a focus on performance and scalability.
11//! To this end, the most critical parts of the algorithm are accelerated by GPU
12//! compute shaders.
13//! The [wgpu](https://github.com/gfx-rs/wgpu) crate is used
14//! for interfacing with the system's GPU API.
15//! Shaders are written in the WebGPU Shading Language
16//! ([WGSL](https://gpuweb.github.io/gpuweb/wgsl/)).
17//! These technologies are based on the up-and-coming WebGPU API,
18//! aiming to enable advanced access to GPUs in web browsers.
19//! Therefore, this crate can be used in WebAssembly,
20//! although it requires the browser to support the WebGPU API.
21//!
22//! Requires **Rust version** >= 1.73.
23//!
24//! ### Equivalences
25//!
26//! For an input Labeled Transition System, the algorithm will decide for any
27//! process pair which of the behavioral equivalences in the following spectrum
28//! hold and which don't. See [`std_equivalences`].
29//!
30//! * Enabledness
31//! * Trace Equivalence
32//! * Failures
33//! * Failure Traces
34//! * Readiness
35//! * Readiness Traces
36//! * Revivals
37//! * Impossible Futures
38//! * Possible Futures
39//! * Simulation
40//! * Ready Simulation
41//! * Nested 2-Simulation
42//! * Bisimulation
43//!
44//! # Usage
45//!
46//! Most of this crate's functionality can be accessed through the
47//! [`TransitionSystem`] struct.
48//! After constructing a transition system,
49//! equivalences can be computed either between two processes
50//! or between all process pairs of the system.
51//!
52//! ### Comparing two processes
53//!
54//! Using [`TransitionSystem::compare()`], the energies for comparing two
55//! processes can be computed.
56//! These energies encode the equivalences between them,
57//! which can be tested using [`EnergyArray::test_equivalence()`]
58//! with the definitions of the various equivalence notions from
59//! [`std_equivalences`].
60//! The following example calculates the equivalences between processes 0 and 4:
61//!
62//!
63//! ```
64//! use gpuequiv::{TransitionSystem, std_equivalences};
65//!
66//! # fn main() {
67//! # pollster::block_on(run());
68//! # }
69//! # async fn run() {
70//! let (a, b, c) = (0, 1, 2);
71//! let lts = TransitionSystem::from(vec![
72//! (0, 1, a), (1, 2, b), (1, 3, c),
73//! (4, 5, a), (5, 7, b), (4, 6, a), (6, 8, c),
74//! ]);
75//!
76//! let energies = lts.compare(0, 4).await.unwrap();
77//! // Process 4 does not simulates process 0
78//! assert!(!energies.test_equivalence(std_equivalences::simulation()));
79//! // Process 4 has all traces that process 0 has
80//! assert!(energies.test_equivalence(std_equivalences::traces()));
81//! # }
82//! ```
83//!
84//! ### Comparing all processes in a system
85//!
86//! Handling the information for equivalences between all process pairs
87//! is a bit more involved.
88//! The function [`TransitionSystem::equivalences()`]
89//! returns an [`Equivalence`] struct,
90//! which can be used to explore equivalences between any two processes,
91//! and even to construct equivalence relations.
92//! Some of the ways in which that can be used are shown below:
93//!
94//! ```
95//! use gpuequiv::{TransitionSystem, std_equivalences};
96//!
97//! # fn main() {
98//! # pollster::block_on(run());
99//! # }
100//! # async fn run() {
101//! let (a, b, c) = (0, 1, 2);
102//! let lts = TransitionSystem::from(vec![
103//! (0, 1, a), (1, 2, b), (1, 3, c),
104//! (4, 5, a), (5, 7, b), (4, 6, a), (6, 8, c),
105//! ]);
106//!
107//! let equivalence = lts.equivalences().await.unwrap();
108//!
109//! // 4 is simulated by 0, but not the other way around, so they are not simulation-equivalent
110//! assert!(equivalence.preorder(4, 0, std_equivalences::simulation()));
111//! assert!(!equivalence.equiv(4, 0, std_equivalences::simulation()));
112//!
113//! // All process pairs can be queried
114//! assert!(!equivalence.equiv(1, 5, std_equivalences::enabledness()));
115//! assert!(equivalence.equiv(3, 7, std_equivalences::bisimulation()));
116//!
117//! // Inspect the trace equivalence relation
118//! let traces = equivalence.relation(std_equivalences::traces());
119//! assert_eq!(traces.count_classes(), 5);
120//!
121//! // All processes that are trace-equivalent with process 0
122//! let mut class_of_0 = traces.class_of(0);
123//! assert_eq!(class_of_0.next(), Some(0));
124//! assert_eq!(class_of_0.next(), Some(4));
125//! assert_eq!(class_of_0.next(), None);
126//! # }
127//! ```
128//!
129//! # Further Examples
130//!
131//! The [`examples`](https://github.com/Gobbel2000/gpuequiv/tree/master/examples)
132//! directory in the repository contains further example files
133//! on how this crate can be used.
134//! [`compare.rs`](https://github.com/Gobbel2000/gpuequiv/blob/master/examples/compare.rs)
135//! and
136//! [`compare_all.rs`](https://github.com/Gobbel2000/gpuequiv/blob/master/examples/compare_all.rs)
137//! further exemplify how this crate can be used to compare two or all processes,
138//! [`energygame.rs`](https://github.com/Gobbel2000/gpuequiv/blob/master/examples/energygame.rs)
139//! solves just an energy game, that was not created from an LTS.
140//!
141//! # Serde
142//!
143//! When compiled with the feature flag `serde` (disabled by default),
144//! the structs [`TransitionSystem`]
145//! and [`GameGraph`](crate::energygame::GameGraph)
146//! implement serde's `Serialize` and `Deserialize` traits.
147
148pub mod energygame;
149pub mod gamebuild;
150pub mod equivalence;
151mod energy;
152mod error;
153
154// Re-exports
155pub use energy::*;
156pub use error::*;
157
158use std::fs::File;
159use std::io::{self, BufRead};
160use std::path::Path;
161use std::result;
162
163use gamebuild::GameBuild;
164use energygame::EnergyGame;
165use equivalence::Equivalence;
166
167use rustc_hash::FxHashMap;
168
169/// A transition to the next process with a label. Part of a labeled transition system.
170#[derive(Debug, Clone, Copy)]
171#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
172pub struct Transition {
173 /// Target process of this transition
174 pub process: u32,
175 /// Transition label encoded as i32
176 ///
177 /// If there are actual names (Strings), they should be stored in a separate list.
178 pub label: i32,
179}
180
181/// Labeled Transition System (LTS) graph
182///
183/// The graph is represented by adjacency lists.
184/// Each adjacent node (or process) is stored together with the transition label
185/// in a [`Transition`] struct.
186///
187/// # Creation
188///
189/// A transition system can be constructed from 2-dimensional adjacency lists of transitions:
190/// `Vec<Vec<Transition>>`.
191/// A more convenient specification is listing all edges as 3-tuples `(u32, u32, i32)`,
192/// which includes start and end nodes as well as the edge label.
193/// The list does not need to be sorted.
194/// For example:
195///
196/// ```
197/// use gpuequiv::TransitionSystem;
198///
199/// // Define label names
200/// let (a, b, c) = (0, 1, 2);
201/// let lts = TransitionSystem::from(vec![
202/// (0, 1, a),
203/// (1, 2, b),
204/// (1, 3, c),
205///
206/// (4, 5, a),
207/// (5, 7, b),
208/// (4, 6, a),
209/// (6, 8, c),
210/// ]);
211/// ```
212///
213/// An LTS can also be read from a CSV file.
214/// See [`from_csv_file()`](TransitionSystem::from_csv_file) for details.
215#[derive(Debug, Clone, Default)]
216#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
217pub struct TransitionSystem {
218 adj: Vec<Vec<Transition>>,
219}
220
221type Edge = (u32, u32, i32);
222
223impl TransitionSystem {
224 /// The number of vertices (or processes) in this transition system.
225 #[inline]
226 pub fn n_vertices(&self) -> u32 {
227 self.adj.len() as u32
228 }
229
230 /// Return the adjacency list,
231 /// containing all available transitions from a process `p`.
232 ///
233 /// # Panics
234 ///
235 /// Panics if `p` is outside the range of processes of this LTS.
236 #[inline]
237 pub fn adj(&self, p: u32) -> &[Transition] {
238 &self.adj[p as usize]
239 }
240
241 /// Run the algorithm to compare processes `p` and `q`.
242 ///
243 /// Returns a set of energies that can be used to determine the equivalences that
244 /// preorder `p` by `q`.
245 ///
246 /// # Example
247 ///
248 /// ```
249 /// use gpuequiv::{TransitionSystem, std_equivalences};
250 /// # fn main() {
251 /// # pollster::block_on(run());
252 /// # }
253 /// # async fn run() {
254 /// let (a, b, c) = (0, 1, 2);
255 /// let lts = TransitionSystem::from(vec![
256 /// (0, 1, a), (1, 2, b), (1, 3, c),
257 /// (4, 5, a), (5, 7, b), (4, 6, a), (6, 8, c),
258 /// ]);
259 ///
260 /// let energies = lts.compare(0, 4).await.unwrap();
261 /// // Process 4 does not simulates process 0
262 /// assert!(!energies.test_equivalence(std_equivalences::simulation()));
263 /// // Process 4 has all traces that process 0 has
264 /// assert!(energies.test_equivalence(std_equivalences::traces()));
265 /// # }
266 /// ```
267 ///
268 /// # Errors
269 ///
270 /// If no connection to a GPU could be made, an error is returned.
271 ///
272 /// # Panics
273 ///
274 /// Panics if `p` or `q` are outside the range of processes of this LTS.
275 pub async fn compare(&self, p: u32, q: u32) -> Result<EnergyArray> {
276 let (reduced, minimization) = self.bisimilar_minimize();
277 let pm = minimization[p as usize] as u32;
278 let qm = minimization[q as usize] as u32;
279 if pm == qm { // p and q are bisimilar, we don't need to do anything
280 return Ok(EnergyArray::empty(GameBuild::ENERGY_CONF));
281 }
282 reduced.compare_unminimized(pm, qm).await
283 }
284
285 /// The same as [`compare()`](TransitionSystem::compare),
286 /// but without minimizing the LTS first.
287 ///
288 /// # Panics
289 ///
290 /// Panics if `p` or `q` are outside the range of processes of this LTS.
291 pub async fn compare_unminimized(&self, p: u32, q: u32) -> Result<EnergyArray> {
292 let builder = GameBuild::compare(self, p, q);
293 let mut game = EnergyGame::standard_reach(builder.game);
294 let energies = game.run().await?;
295 Ok(energies.iter().next().unwrap().clone())
296 }
297
298 /// Find all equivalences for all process pairs in the LTS.
299 ///
300 /// Returns an [`Equivalence`] struct which can be used to explore the results.
301 /// See the documentation of [`Equivalence`] for further details.
302 ///
303 /// # Example
304 ///
305 /// ```
306 /// use gpuequiv::{TransitionSystem, std_equivalences};
307 /// # fn main() {
308 /// # pollster::block_on(run());
309 /// # }
310 /// # async fn run() {
311 /// let (a, b, c) = (0, 1, 2);
312 /// let lts = TransitionSystem::from(vec![
313 /// (0, 1, a), (1, 2, b), (1, 3, c),
314 /// (4, 5, a), (5, 7, b), (4, 6, a), (6, 8, c),
315 /// ]);
316 ///
317 /// let equivalence = lts.equivalences().await.unwrap();
318 /// // All processes can now be compared
319 /// assert!(!equivalence.preorder(0, 4, std_equivalences::simulation()));
320 /// assert!(equivalence.equiv(0, 4, std_equivalences::traces()));
321 /// assert!(!equivalence.equiv(1, 5, std_equivalences::enabledness()));
322 /// assert!(equivalence.equiv(3, 7, std_equivalences::bisimulation()));
323 /// # }
324 /// ```
325 ///
326 /// # Errors
327 ///
328 /// If no connection to a GPU could be made, an error is returned.
329 pub async fn equivalences(&self) -> Result<Equivalence> {
330 let (reduced, minimization) = self.bisimilar_minimize();
331 let mut equivalence = reduced.equivalences_unminimized().await?;
332 equivalence.set_minimization(minimization);
333 Ok(equivalence)
334 }
335
336 /// The same as [`equivalences()`](TransitionSystem::equivalences),
337 /// but without minimizing the LTS first.
338 pub async fn equivalences_unminimized(&self) -> Result<Equivalence> {
339 let (builder, start_info) = GameBuild::compare_all(self);
340 let mut game = EnergyGame::standard_reach(builder.game);
341 game.run().await?;
342 Ok(start_info.equivalence(game.energies))
343 }
344
345 /// Build a labeled transition system from a CSV file.
346 ///
347 /// Each line should represent an edge of the graph,
348 /// containing the three elements start process, end process and label.
349 /// For example the line
350 ///
351 /// ```text
352 /// 4,10,"enter"
353 /// ````
354 ///
355 /// encodes a transition from process `4` to process `10` using the action `enter`.
356 /// Quotes around the label name are optional.
357 /// Note that these label names are not stored. They are replaced by unique integers.
358 ///
359 /// # Errors
360 ///
361 /// A [`CSVError`] is returned if a line does not contain 3 fields,
362 /// if any of the first two fields can not be parsed as an unsigned integer,
363 /// or if there were problems reading the file.
364 pub fn from_csv_file<P: AsRef<Path>>(path: P) -> result::Result<Self, CSVError> {
365 let file = File::open(path)?;
366 let lines = io::BufReader::new(file).lines();
367 Self::from_csv_lines(lines)
368 }
369
370 /// Build a transition system from an iterator of CSV lines.
371 /// The format is described at [`from_csv_file()`](TransitionSystem::from_csv_file).
372 pub fn from_csv_lines(lines: impl Iterator<Item=io::Result<String>>
373 ) -> result::Result<Self, CSVError> {
374 let mut adj = vec![];
375 let mut labels: FxHashMap<String, i32> = FxHashMap::default();
376 labels.insert("i".to_string(), 0);
377 let mut max_label = 0;
378 for l in lines {
379 let l = l?;
380 // Disassemble line
381 let mut parts = l.splitn(3, ',');
382 let from: usize = parts.next().ok_or(CSVError::MissingField)?.parse()?;
383 let to: u32 = parts.next().ok_or(CSVError::MissingField)?.parse()?;
384 let mut label = parts.next().ok_or(CSVError::MissingField)?;
385
386 // Strip quotation marks
387 if label.starts_with('"') && label.ends_with('"') && label.len() >= 2 {
388 label = &label[1 .. label.len() - 1];
389 }
390
391 // Find the integer for the label
392 let label_n = labels.get(label).copied().unwrap_or_else(|| {
393 max_label += 1;
394 labels.insert(label.to_string(), max_label);
395 max_label
396 });
397
398 // If necessary, grow adjacency table to accommodate all mentioned nodes
399 let max_node = from.max(to as usize);
400 if max_node >= adj.len() {
401 adj.resize(max_node + 1, vec![]);
402 }
403 adj[from].push(Transition { process: to, label: label_n });
404 }
405 Ok(TransitionSystem::from(adj))
406 }
407
408 /// Create a new, minimized LTS where bisimilar processes are consolidated.
409 ///
410 /// Returns a tuple containing the minimized system and the bisimulation used.
411 /// This bisimulation is a mapping from original processes to the process in the minimized LTS
412 /// that covers its bisimilarity class.
413 pub fn bisimilar_minimize(&self) -> (TransitionSystem, Vec<usize>) {
414 let (bisim, count) = self.signature_refinement();
415 let mut adj = vec![vec![]; count];
416 let mut represented = vec![false; count];
417 for (process, &partition) in bisim.iter().enumerate() {
418 if !represented[partition] {
419 if adj.len() <= partition {
420 adj.resize(partition + 1, Vec::new());
421 }
422 adj[partition] = self.adj[process].iter()
423 .map(|transition| Transition {
424 process: bisim[transition.process as usize] as u32,
425 label: transition.label,
426 })
427 .collect();
428 represented[partition] = true;
429 }
430 }
431 (adj.into(), bisim)
432 }
433
434 /// Find strong bisimulation using signature refinement.
435 /// The first return value is the partition, the second the number of bisimilarity classes.
436 ///
437 /// The partition list contains an index of the bisimulation equivalence class for each process.
438 /// That is, if `p = signature_refinement()`, then `p[i] == p[j]` iff `i ~ j`.
439 /// In other words, if and only if two processes get assigned the same partition index,
440 /// they are bisimilar.
441 ///
442 /// This sequential algorithm is described by S. Blom and S. Orzan in
443 /// "A Distributed Algorithm for Strong Bisimulation Reduction of State Spaces", 2002.
444 pub fn signature_refinement(&self) -> (Vec<usize>, usize) {
445 let mut partition = vec![0; self.n_vertices() as usize];
446 let mut prev_count: usize = 0;
447 let mut new_count: usize = 1;
448 while prev_count != new_count {
449 prev_count = new_count;
450 new_count = 0;
451 let signatures = self.signatures(&partition);
452 let mut sigmap = FxHashMap::default();
453 for sig in &signatures {
454 sigmap.entry(sig).or_insert_with(|| {
455 new_count += 1;
456 new_count - 1
457 });
458 }
459 for (part, sig) in partition.iter_mut().zip(&signatures) {
460 *part = sigmap[&sig];
461 }
462 }
463 (partition, new_count)
464 }
465
466 // Returns a set-valued signature for each process i:
467 // sig[i] = {(a, ID) | i -a-> j and partition[j] == ID}
468 fn signatures(&self, partition: &[usize]) -> Vec<Vec<(i32, usize)>> {
469 self.adj.iter().map(|adj| {
470 let mut sig: Vec<_> = adj.iter()
471 .map(|transition| (transition.label, partition[transition.process as usize]))
472 .collect();
473 sig.sort_unstable();
474 sig.dedup();
475 sig
476 })
477 .collect()
478 }
479
480 /// Create LTS from adjacency lists.
481 ///
482 /// This function assumes that the lists in `adj` are already
483 /// sorted by label. If not, the algorithm will produce incorrect results. Use
484 /// [`TransitionSystem::from()`] instead to ensure correct sorting.
485 pub fn from_adj_unchecked(adj: Vec<Vec<Transition>>) -> Self {
486 TransitionSystem { adj }
487 }
488
489 fn sort_labels(&mut self) {
490 for row in &mut self.adj {
491 row.sort_by_key(|t| t.label);
492 }
493 }
494
495 /// Return a reference to the inner adjacency lists.
496 #[inline]
497 pub fn inner(&self) -> &[Vec<Transition>] {
498 &self.adj
499 }
500
501 /// Take ownership of the inner adjacency lists.
502 #[inline]
503 pub fn into_inner(self) -> Vec<Vec<Transition>> {
504 self.adj
505 }
506}
507
508impl From<Vec<Vec<Transition>>> for TransitionSystem {
509 /// Create new LTS from adjacency lists, ensuring that they are properly sorted.
510 fn from(adj: Vec<Vec<Transition>>) -> Self {
511 let mut lts = TransitionSystem::from_adj_unchecked(adj);
512 lts.sort_labels();
513 lts
514 }
515}
516
517impl From<Vec<Edge>> for TransitionSystem {
518 fn from(edges: Vec<Edge>) -> Self {
519 edges.into_iter().collect()
520 }
521}
522
523impl FromIterator<Edge> for TransitionSystem {
524 fn from_iter<I: IntoIterator<Item=Edge>>(iter: I) -> Self {
525 let mut adj = vec![];
526 for (from, to, label) in iter {
527 let from = from as usize;
528 let max_node = from.max(to as usize);
529 if max_node >= adj.len() {
530 adj.resize(max_node + 1, vec![]);
531 }
532 adj[from].push(Transition { process: to, label });
533 }
534 TransitionSystem::from(adj)
535 }
536}