From 5192375f140b6e1e73ef1218c0e0d91908077606 Mon Sep 17 00:00:00 2001 From: Emily Schmidt Date: Tue, 15 Oct 2024 18:11:24 +0100 Subject: [PATCH] clean up dsf crate; move IdAlloc to ids crate --- Cargo.lock | 1 - dsf/Cargo.toml | 1 - dsf/src/element.rs | 57 ++++ dsf/src/lib.rs | 13 +- dsf/src/tests/test_tracked_union_find.rs | 32 ++ dsf/src/tests/test_union_find.rs | 176 +++++++++++ dsf/src/tracked_union_find.rs | 256 +++++++++------ dsf/src/union_find.rs | 378 +++++++++-------------- ids/src/id_alloc.rs | 54 ++++ ids/src/lib.rs | 3 + 10 files changed, 654 insertions(+), 317 deletions(-) create mode 100644 dsf/src/element.rs create mode 100644 dsf/src/tests/test_tracked_union_find.rs create mode 100644 dsf/src/tests/test_union_find.rs create mode 100644 ids/src/id_alloc.rs diff --git a/Cargo.lock b/Cargo.lock index 98b0d04..0da409e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,7 +257,6 @@ name = "dsf" version = "0.1.0" dependencies = [ "atomic", - "bytemuck", "imctk-ids", "imctk-lit", "priority-queue", diff --git a/dsf/Cargo.toml b/dsf/Cargo.toml index bee1698..f4fd701 100644 --- a/dsf/Cargo.toml +++ b/dsf/Cargo.toml @@ -8,7 +8,6 @@ publish = false [dependencies] imctk-ids = { version = "0.1.0", path = "../ids" } imctk-lit = { version = "0.1.0", path = "../lit" } -bytemuck = "*" atomic = "0.6" priority-queue = "*" diff --git a/dsf/src/element.rs b/dsf/src/element.rs new file mode 100644 index 0000000..7bb6809 --- /dev/null +++ b/dsf/src/element.rs @@ -0,0 +1,57 @@ +//! A trait for "elements" that can be split into an "atom" and a "polarity". + +use imctk_lit::{Lit, Var}; + +/// A trait for "elements" that can be split into an "atom" and a "polarity". +/// +/// This lets code generically manipulate variables and literals, and further serves to abstract over their concrete representation. +/// +/// The two most common case this trait is used for are: +/// 1) The element and the atom are both `Var`. In this case there is only one polarity and the trait implementation is trivial. +/// 2) The element is `Lit` and the atom is `Var`. Here there are two polarities (`+` and `-`) to keep track of. +/// +/// Mathematically, implementing this trait signifies that elements can be written as pairs `(a, p)` with an atom `a` and a polarity `p`. +/// The polarities are assumed to form a group `(P, *, 1)`. The trait operations then correspond to: +/// 1) `from_atom(a) = (a, 1)` +/// 2) `atom((a, p)) = a` +/// 3) `apply_pol_of((a, p), (b, q)) = (a, p * q)` +/// +/// Currently, code assumes that `P` is either trivial or isomorphic to `Z_2`. +/// +/// Code using this trait may assume the following axioms to hold: +/// 1) `from_atom(atom(x)) == x` +/// 2) `apply_pol_of(atom(x), x) == x` +/// 3) `apply_pol_of(apply_pol_of(x, y), y) == x` +// TODO: add missing axioms +pub trait Element { + /// Constructs an element from an atom by applying positive polarity. + fn from_atom(atom: Atom) -> Self; + /// Returns the atom corresponding to an element. + fn atom(self) -> Atom; + /// Multiplies `self` by the polarity of `other`, i.e. conceptually `apply_pol_of(self, other) = self ^ pol(other)`. + fn apply_pol_of(self, other: Self) -> Self; +} + +impl Element for T { + fn from_atom(atom: T) -> Self { + atom + } + fn atom(self) -> T { + self + } + fn apply_pol_of(self, _other: T) -> Self { + self + } +} + +impl Element for Lit { + fn from_atom(atom: Var) -> Self { + atom.as_lit() + } + fn atom(self) -> Var { + self.var() + } + fn apply_pol_of(self, other: Self) -> Self { + self ^ other.pol() + } +} \ No newline at end of file diff --git a/dsf/src/lib.rs b/dsf/src/lib.rs index 0505e00..f4f90ca 100644 --- a/dsf/src/lib.rs +++ b/dsf/src/lib.rs @@ -1,2 +1,13 @@ +//! This crate defines a structure [`UnionFind`] that allows tracking of equivalences between generic elements +//! and a structure [`TrackedUnionFind`] that provides the same functionality but augmented by change tracking. + +#[doc(inline)] +pub use element::Element; +#[doc(inline)] +pub use tracked_union_find::TrackedUnionFind; +#[doc(inline)] +pub use union_find::UnionFind; + +pub mod element; +pub mod tracked_union_find; pub mod union_find; -pub mod tracked_union_find; \ No newline at end of file diff --git a/dsf/src/tests/test_tracked_union_find.rs b/dsf/src/tests/test_tracked_union_find.rs new file mode 100644 index 0000000..24f8d5c --- /dev/null +++ b/dsf/src/tests/test_tracked_union_find.rs @@ -0,0 +1,32 @@ +use super::*; +use imctk_lit::{Lit, Var}; + +#[test] +fn test() { + let l = |n| Var::from_index(n).as_lit(); + let mut tuf = TrackedUnionFind::::new(); + let mut token = tuf.start_observing(); + tuf.union([l(3), !l(4)]); + tuf.union([l(8), l(7)]); + let mut token2 = tuf.start_observing(); + tuf.union([l(4), l(5)]); + for change in tuf.drain_changes(&mut token).cloned() { + println!("{change:?}"); + } + println!("---"); + tuf.union([!l(5), l(6)]); + tuf.make_repr(l(4).var()); + let renumber: IdVec> = + IdVec::from_vec(vec![Some(l(0)), None, None, Some(l(1)), Some(!l(1)), Some(!l(1)), Some(l(1)), Some(l(2)), Some(l(2))]); + let reverse = Renumbering::get_reverse(&renumber, &tuf.union_find); + dbg!(&renumber, &reverse); + tuf.renumber(renumber, reverse); + tuf.union([l(0), l(1)]); + let mut iter = tuf.drain_changes(&mut token); + println!("{:?}", iter.next()); + iter.stop(); + println!("---"); + for change in tuf.drain_changes(&mut token2).cloned() { + println!("{change:?}"); + } +} diff --git a/dsf/src/tests/test_union_find.rs b/dsf/src/tests/test_union_find.rs new file mode 100644 index 0000000..142a245 --- /dev/null +++ b/dsf/src/tests/test_union_find.rs @@ -0,0 +1,176 @@ +#![allow(dead_code, missing_docs)] + +use super::*; +use imctk_lit::{Var, Lit}; +use imctk_ids::id_set_seq::IdSetSeq; +use rand::prelude::*; +use std::collections::{HashSet, VecDeque}; + +#[derive(Default)] +struct CheckedUnionFind { + dut: UnionFind, + equivs: IdSetSeq, +} + +impl> UnionFind { + fn debug_print_tree( + children: &IdVec>, + atom: Atom, + prefix: &str, + self_char: &str, + further_char: &str, + pol: bool, + ) { + println!( + "{prefix}{self_char}{}{:?}", + if pol { "!" } else { "" }, + atom + ); + let my_children = children.get(atom).unwrap(); + for (index, &child) in my_children.iter().enumerate() { + let last = index == my_children.len() - 1; + let self_char = if last { "└" } else { "├" }; + let next_further_char = if last { " " } else { "│" }; + Self::debug_print_tree( + children, + child.atom(), + &(prefix.to_string() + further_char), + self_char, + next_further_char, + pol ^ (child != Elem::from_atom(child.atom())), + ); + } + } + fn debug_print(&self) { + let mut children: IdVec> = Default::default(); + for atom in self.parent.keys() { + let parent = self.read_parent(atom); + children.grow_for_key(atom); + if atom != parent.atom() { + children + .grow_for_key(parent.atom()) + .push(Elem::from_atom(atom).apply_pol_of(parent)); + } else { + assert!(Elem::from_atom(atom) == parent); + } + } + for atom in self.parent.keys() { + if atom == self.read_parent(atom).atom() { + Self::debug_print_tree(&children, atom, "", "", " ", false); + } + } + } +} +#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] +enum VarRel { + Equiv, + AntiEquiv, + NotEquiv, +} + +impl> CheckedUnionFind { + fn new() -> Self { + CheckedUnionFind { + dut: Default::default(), + equivs: Default::default(), + } + } + fn ref_equal(&mut self, start: Elem, goal: Elem) -> VarRel { + let mut seen: HashSet = Default::default(); + let mut queue: VecDeque = [start].into(); + while let Some(place) = queue.pop_front() { + if place.atom() == goal.atom() { + if place == goal { + return VarRel::Equiv; + } else { + return VarRel::AntiEquiv; + } + } + seen.insert(place.atom()); + for &next in self.equivs.grow_for(place.atom()).iter() { + if !seen.contains(&next.atom()) { + queue.push_back(next.apply_pol_of(place)); + } + } + } + VarRel::NotEquiv + } + fn find(&mut self, lit: Elem) -> Elem { + let out = self.dut.find(lit); + assert!(self.ref_equal(lit, out) == VarRel::Equiv); + out + } + fn union_full(&mut self, lits: [Elem; 2]) -> (bool, [Elem; 2]) { + let (ok, [ra, rb]) = self.dut.union_full(lits); + assert_eq!(self.ref_equal(lits[0], ra), VarRel::Equiv); + assert_eq!(self.ref_equal(lits[1], rb), VarRel::Equiv); + assert_eq!(ok, self.ref_equal(lits[0], lits[1]) == VarRel::NotEquiv); + assert_eq!(self.dut.find_root(lits[0]), ra); + if ok { + assert_eq!(self.dut.find_root(lits[1]), ra); + self.equivs + .grow_for(lits[0].atom()) + .insert(lits[1].apply_pol_of(lits[0])); + self.equivs + .grow_for(lits[1].atom()) + .insert(lits[0].apply_pol_of(lits[1])); + } else { + assert_eq!(self.dut.find_root(lits[1]).atom(), ra.atom()); + } + (ok, [ra, rb]) + } + fn union(&mut self, lits: [Elem; 2]) -> bool { + self.union_full(lits).0 + } + fn make_repr(&mut self, lit: Atom) { + self.dut.make_repr(lit); + assert_eq!( + self.dut.find_root(Elem::from_atom(lit)), + Elem::from_atom(lit) + ); + self.check(); + } + fn check(&mut self) { + for atom in self.dut.parent.keys() { + let parent = self.dut.read_parent(atom); + assert_eq!(self.ref_equal(Elem::from_atom(atom), parent), VarRel::Equiv); + let root = self.dut.find_root(Elem::from_atom(atom)); + for &child in self.equivs.grow_for(atom).iter() { + assert_eq!(root, self.dut.find_root(child)); + } + } + } +} + +#[test] +fn test() { + let mut u: CheckedUnionFind = CheckedUnionFind::new(); + let mut rng = rand_pcg::Pcg64::seed_from_u64(25); + let max_var = 2000; + for i in 0..2000 { + match rng.gen_range(0..10) { + 0..=4 => { + let a = Lit::from_code(rng.gen_range(0..=2 * max_var + 1)); + let b = Lit::from_code(rng.gen_range(0..=2 * max_var + 1)); + let result = u.union_full([a, b]); + println!("union({a}, {b}) = {result:?}"); + } + 5..=7 => { + let a = Lit::from_code(rng.gen_range(0..=2 * max_var + 1)); + let result = u.find(a); + println!("find({a}) = {result}"); + } + 8 => { + u.check(); + } + 9 => { + let a = Var::from_index(rng.gen_range(0..=max_var)); + u.make_repr(a); + println!("make_repr({a})"); + } + _ => {} + } + } + u.check(); + //u.dut.debug_print(); +} diff --git a/dsf/src/tracked_union_find.rs b/dsf/src/tracked_union_find.rs index 81f47d0..64bbfe5 100644 --- a/dsf/src/tracked_union_find.rs +++ b/dsf/src/tracked_union_find.rs @@ -1,28 +1,29 @@ -#![allow(missing_docs)] -#![allow(clippy::type_complexity)] -use std::{cmp::Reverse, collections::VecDeque, mem::ManuallyDrop, sync::Arc}; +//! A `TrackedUnionFind` augments a [`UnionFind`] structure with change tracking. +use std::{cmp::Reverse, collections::VecDeque, iter::FusedIterator, mem::ManuallyDrop, sync::Arc}; -use atomic::Atomic; -use bytemuck::NoUninit; -use imctk_ids::{id_vec::IdVec, Id, Id64}; +use imctk_ids::{id_vec::IdVec, Id, Id64, IdAlloc}; use priority_queue::PriorityQueue; -use crate::union_find::{Element, UnionFind}; +use crate::{Element, UnionFind}; +#[cfg(test)] +#[path = "tests/test_tracked_union_find.rs"] +mod test_tracked_union_find; + +/// Globally unique ID for a `TrackedUnionFind`. #[derive(Id, Debug)] #[repr(transparent)] pub struct TrackedUnionFindId(Id64); -// SAFETY: trust me bro -unsafe impl NoUninit for TrackedUnionFindId {} +/// Generation number for a `TrackedUnionFind` #[derive(Id, Debug)] #[repr(transparent)] pub struct Generation(u64); +/// Observer ID for a `TrackedUnionFind` (not globally unique) #[derive(Id, Debug)] #[repr(transparent)] pub struct ObserverId(Id64); -// SAFETY: trust me bro -unsafe impl NoUninit for ObserverId {} +/// An `ObserverToken` represents an observer of a `TrackedUnionFind`. #[derive(Debug)] pub struct ObserverToken { tuf_id: TrackedUnionFindId, @@ -30,6 +31,44 @@ pub struct ObserverToken { observer_id: ObserverId, } +impl ObserverToken { + /// Returns the ID of the associated `TrackedUnionFind`. + pub fn tuf_id(&self) -> TrackedUnionFindId { + self.tuf_id + } + /// Returns the ID of the generation that this observer is on. + pub fn generation(&self) -> Generation { + self.generation + } + /// Returns the ID of the observer. + /// + /// Note that observer IDs are local to each `TrackedUnionFind`. + pub fn observer_id(&self) -> ObserverId { + self.observer_id + } + /// Returns `true` iff `self` and `other` belong to the same `TrackedUnionFind`. + /// + /// NB: This does **not** imply that variable IDs are compatible, for that you want `is_compatible`. + pub fn is_same_tuf(&self, other: &ObserverToken) -> bool { + self.tuf_id == other.tuf_id + } + /// Returns `true` iff `self` and `other` have compatible variable IDs. + /// + /// This is equivalent to whether they are on the same generation of the same `TrackedUnionFind`. + pub fn is_compatible(&self, other: &ObserverToken) -> bool { + self.tuf_id == other.tuf_id && self.generation == other.generation + } +} + +/// `Renumbering` represents a renumbering of all variables in `TrackedUnionFind`. +/// +/// A renumbering stores a forward and a reverse mapping, and the old and new generation IDs. +/// +/// The forward mapping maps each old variable to an optional new variable (variables may be deleted). +/// It is a requirement that equivalent old variables are either both mapped to the same new variable or both deleted. +/// +/// The reverse mapping maps each new variable to its old representative. +/// The new set of variables is required to be contiguous, hence `reverse` is a total mapping. pub struct Renumbering { forward: IdVec>, reverse: IdVec, @@ -48,8 +87,12 @@ impl std::fmt::Debug for Renumbering { } } -impl + NoUninit> Renumbering { - pub fn get_reverse(forward: &IdVec>, union_find: &UnionFind) -> IdVec { +impl> Renumbering { + /// Returns the inverse of a reassignment of variables. + pub fn get_reverse( + forward: &IdVec>, + union_find: &UnionFind, + ) -> IdVec { let mut reverse: IdVec> = IdVec::default(); for (old, &new_opt) in forward { if let Some(new) = new_opt { @@ -60,6 +103,7 @@ impl + NoUninit> Renumbering { } IdVec::from_vec(reverse.iter().map(|x| x.1.unwrap()).collect()) } + /// Returns `true` iff the arguments are inverses of each other. pub fn is_inverse(forward: &IdVec>, reverse: &IdVec) -> bool { reverse.iter().all(|(new, &old)| { if let Some(&Some(e)) = forward.get(old.atom()) { @@ -69,6 +113,7 @@ impl + NoUninit> Renumbering { } }) } + /// Creates a renumbering without checking whether the arguments are valid. pub fn new_unchecked( forward: IdVec>, reverse: IdVec, @@ -82,6 +127,7 @@ impl + NoUninit> Renumbering { new_generation, } } + /// Creates a new renumbering from the given forward and reverse assignment. pub fn new( forward: IdVec>, reverse: IdVec, @@ -92,6 +138,7 @@ impl + NoUninit> Renumbering { debug_assert!(Self::is_inverse(&forward, &reverse)); Self::new_unchecked(forward, reverse, old_generation, new_generation) } + /// Returns the new variable corresponding to the given old variable, if it exists. pub fn old_to_new(&self, old: Elem) -> Option { self.forward .get(old.atom()) @@ -99,9 +146,11 @@ impl + NoUninit> Renumbering { .flatten() .map(|e| e.apply_pol_of(old)) } + /// Returns the old variable corresponding to the given new variable, if it exists. pub fn new_to_old(&self, new: Elem) -> Option { self.reverse.get(new.atom()).map(|&e| e.apply_pol_of(new)) } + /// Returns `true` iff the given renumbering satisfies the constraint that equivalent variables are mapped identically. pub fn is_repr_reduction(&self, union_find: &UnionFind) -> bool { union_find.lowest_unused_atom() <= self.forward.next_unused_key() && self.forward.iter().all(|(old, &new)| { @@ -112,10 +161,15 @@ impl + NoUninit> Renumbering { } } +/// `Change` represents a single change of a `TrackedUnionFind` +#[allow(missing_docs)] // dont want to document every subfield #[derive(Clone)] pub enum Change { + /// A `union` operation. The set with representative `merged_repr` is merged into the set with representative `new_repr`. Union { new_repr: Atom, merged_repr: Elem }, + /// A `make_repr` operation. `new_repr` is promoted to be the representative of its set, replacing `old_repr`. MakeRepr { new_repr: Atom, old_repr: Elem }, + /// A renumbering operation. Renumber(Arc>), } @@ -140,6 +194,7 @@ impl std::fmt::Debug for Change { } } +/// A `TrackedUnionFind` augments a [`UnionFind`] structure with change tracking. pub struct TrackedUnionFind { tuf_id: TrackedUnionFindId, union_find: UnionFind, @@ -150,46 +205,13 @@ pub struct TrackedUnionFind { generation: Generation, } -pub struct IdAlloc { - counter: Atomic, -} - -impl Default for IdAlloc { - fn default() -> Self { - Self::new() - } -} - -impl IdAlloc { - const fn new() -> Self { - Self { - counter: Atomic::new(T::MIN_ID), - } - } - pub fn alloc_block(&self, n: usize) -> T { - use atomic::Ordering::Relaxed; - debug_assert!(n > 0); - self.counter - .fetch_update(Relaxed, Relaxed, |current_id| { - current_id - .id_index() - .checked_add(n) - .and_then(T::try_from_id_index) - }) - .expect("not enough IDs remaining") - } - pub fn alloc(&self) -> T { - self.alloc_block(1) - } -} - static TUF_ID_ALLOC: IdAlloc = IdAlloc::new(); -impl Default for TrackedUnionFind { - fn default() -> Self { +impl From> for TrackedUnionFind { + fn from(union_find: UnionFind) -> Self { Self { - tuf_id: TUF_ID_ALLOC.alloc(), - union_find: Default::default(), + union_find, + tuf_id: TUF_ID_ALLOC.alloc().unwrap(), log: Default::default(), observer_id_alloc: Default::default(), observers: Default::default(), @@ -199,12 +221,31 @@ impl Default for TrackedUnionFind { } } -impl + NoUninit> TrackedUnionFind { +impl Default for TrackedUnionFind { + fn default() -> Self { + UnionFind::default().into() + } +} + +impl TrackedUnionFind { + /// Returns a shared reference to the contained `UnionFind`. + pub fn get_union_find(&self) -> &UnionFind { + &self.union_find + } + /// Returns the contained `UnionFind`. All change tracking data is lost. + pub fn into_union_find(self) -> UnionFind { + self.union_find + } +} + +impl> TrackedUnionFind { + /// Returns an element's representative. See [`UnionFind::find`]. pub fn find(&self, elem: Elem) -> Elem { self.union_find.find(elem) } - pub fn union_full(&mut self, lits: [Elem; 2]) -> (bool, [Elem; 2]) { - let (ok, roots) = self.union_find.union_full(lits); + /// Declares two elements to be equivalent. See [`UnionFind::union_full`]. + pub fn union_full(&mut self, elems: [Elem; 2]) -> (bool, [Elem; 2]) { + let (ok, roots) = self.union_find.union_full(elems); if ok && !self.observers.is_empty() { let new_repr = roots[0].atom(); let merged_repr = roots[1].apply_pol_of(roots[0]); @@ -215,9 +256,11 @@ impl + NoUninit> TrackedUnionFind } (ok, roots) } + /// Declares two elements to be equivalent. See [`UnionFind::union`]. pub fn union(&mut self, lits: [Elem; 2]) -> bool { self.union_full(lits).0 } + /// Promotes an atom to be a representative. See [`UnionFind::make_repr`]. pub fn make_repr(&mut self, new_repr: Atom) -> Elem { let old_repr = self.union_find.make_repr(new_repr); if old_repr.atom() != new_repr && !self.observers.is_empty() { @@ -225,6 +268,12 @@ impl + NoUninit> TrackedUnionFind } old_repr } + /// Renumbers all the variables in the `UnionFind`. + /// The provided mapping **must** meet all the preconditions listed for [`Renumbering`]. + /// + /// This resets the `UnionFind` to the trivial state (`find(a) == a` for all `a`) and increments the generation ID. + /// + /// This method will panic in debug mode if said preconditions are not met. pub fn renumber(&mut self, forward: IdVec>, reverse: IdVec) { let old_generation = self.generation; let new_generation = Generation(old_generation.0 + 1); @@ -239,14 +288,23 @@ impl + NoUninit> TrackedUnionFind } impl TrackedUnionFind { + /// Constructs a new, empty `TrackedUnionFind`. pub fn new() -> Self { Self::default() } fn log_end(&self) -> u64 { self.log_start + self.log.len() as u64 } + /// Creates a new `ObserverToken` that can be used to track all changes since the call to this method. + /// + /// Conceptually, each observer has its own private log. + /// Any changes that happen to the `UnionFind` will be recorded into the logs of all currently active observers. + /// (In the actual implementation, only a single log is kept). + /// + /// After use, the `ObserverToken` must be disposed of with a call to `stop_observing`, otherwise + /// the memory corresponding to old log entries cannot be reclaimed until the `TrackedUnionFind` is dropped. pub fn start_observing(&mut self) -> ObserverToken { - let observer_id = self.observer_id_alloc.alloc(); + let observer_id = self.observer_id_alloc.alloc().unwrap(); self.observers.push(observer_id, Reverse(self.log_end())); ObserverToken { tuf_id: self.tuf_id, @@ -254,9 +312,10 @@ impl TrackedUnionFind { observer_id, } } + /// Clones an `ObserverToken`, conceptually cloning the token's private log. pub fn clone_token(&mut self, token: &ObserverToken) -> ObserverToken { assert!(token.tuf_id == self.tuf_id); - let new_observer_id = self.observer_id_alloc.alloc(); + let new_observer_id = self.observer_id_alloc.alloc().unwrap(); let pos = *self.observers.get_priority(&token.observer_id).unwrap(); self.observers.push(new_observer_id, pos); ObserverToken { @@ -265,6 +324,9 @@ impl TrackedUnionFind { observer_id: new_observer_id, } } + /// Deletes an `ObserverToken` and its associated state. + /// + /// You must call this to allow the `TrackedUnionFind` to allow memory to be reclaimed. pub fn stop_observing(&mut self, token: ObserverToken) { assert!(token.tuf_id == self.tuf_id); self.observers.remove(&token.observer_id); @@ -275,13 +337,11 @@ impl TrackedUnionFind { if new_start > self.log_start { let delete = (new_start - self.log_start).try_into().unwrap(); drop(self.log.drain(0..delete)); - println!("dropped {delete} entries"); self.log_start = new_start; } } else { self.log_start = self.log_end(); self.log.clear(); - println!("dropped all entries"); } } fn observer_rel_pos(&self, token: &ObserverToken) -> usize { @@ -307,6 +367,7 @@ impl TrackedUnionFind { .change_priority(&token.observer_id, Reverse(abs_pos)); self.truncate_log(); } + #[allow(clippy::type_complexity)] fn change_slices( &self, token: &ObserverToken, @@ -331,6 +392,17 @@ impl TrackedUnionFind { } } } + /// Calls the provided function `f` with the content of the token's private log and clears the log. + /// + /// Because the log is not necessarily contiguous in memory, `f` may be called multiple times. + /// + /// The slice argument to `f` is guaranteed to be non-empty. + /// To allow looking up representatives `f` is also provided with a shared reference to the `UnionFind`. + /// + /// The method assumes that you will immediately process any `Renumbering` operations in the log + /// and will update the token's generation field. + /// + /// Returns `true` iff `f` has been called at least once. pub fn drain_changes_with_fn( &mut self, token: &mut ObserverToken, @@ -351,6 +423,11 @@ impl TrackedUnionFind { false } } + /// Returns a draining iterator that returns and deletes entries from the token's private log. + /// + /// Dropping this iterator will clear any unread entries, call `stop` if this is undesirable. + /// + /// You must not leak the returned iterator. Otherwise log entries may be observed multiple times and appear duplicated. pub fn drain_changes<'a>( &mut self, token: &'a mut ObserverToken, @@ -365,21 +442,37 @@ impl TrackedUnionFind { } } +/// A draining iterator. +/// +/// Since this is a lending iterator, it does not implement the standard `Iterator` trait, +/// but its `map` and `cloned` methods will create a standard iterator. pub struct DrainChanges<'a, 'b, Atom, Elem> { tuf: &'a mut TrackedUnionFind, token: &'b mut ObserverToken, rel_pos: usize, } +/// A draining iterator that has been mapped. pub struct DrainChangesMap<'a, 'b, Atom, Elem, F> { inner: DrainChanges<'a, 'b, Atom, Elem>, f: F, } impl<'a, 'b, Atom, Elem> DrainChanges<'a, 'b, Atom, Elem> { + /// Returns a reference to the first entry in the token's private log, without deleting it. + /// + /// Returns `None` if the log is empty. pub fn peek(&mut self) -> Option<&Change> { self.tuf.log.get(self.rel_pos) } + /// Returns a reference to the first entry in the token's private log. The entry will be deleted after its use. + /// + /// If this returns a `Renumbering`, it is assumed that you will process it and the token's generation number will be updated. + /// + /// Returns `None` if the log is empty. If `next` returned `None`, it will never return any more entries (the iterator is fused). + /// + /// As reflected by the lifetimes, the API only guarantees that the returned reference until the next call of any method of this iterator. + /// (In practice, deletion is more lazy and happens on drop). #[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> Option<&Change> { let ret = self.tuf.log.get(self.rel_pos); @@ -390,14 +483,20 @@ impl<'a, 'b, Atom, Elem> DrainChanges<'a, 'b, Atom, Elem> { } ret } + /// Drops the iterator but without deleting unread entries. pub fn stop(self) { self.tuf.observer_set_rel_pos(self.token, self.rel_pos); let _ = ManuallyDrop::new(self); } + /// Returns `(n, Some(n))` where `n` is the number of unread entries. + /// + /// This method is designed to be compatible with the standard iterator method of the same name. pub fn size_hint(&self) -> (usize, Option) { let count = self.tuf.log.len() - self.rel_pos; (count, Some(count)) } + /// Creates a new iterator by lazily calling `f` on every change. + #[must_use] pub fn map(self, f: F) -> DrainChangesMap<'a, 'b, Atom, Elem, F> where F: FnMut(&Change) -> B, @@ -407,6 +506,9 @@ impl<'a, 'b, Atom, Elem> DrainChanges<'a, 'b, Atom, Elem> { } impl<'a, 'b, Atom: Clone, Elem: Clone> DrainChanges<'a, 'b, Atom, Elem> { + /// Create a standard iterator by cloning every entry. + #[must_use] + #[allow(clippy::type_complexity)] pub fn cloned( self, ) -> DrainChangesMap<'a, 'b, Atom, Elem, fn(&Change) -> Change> { @@ -416,6 +518,9 @@ impl<'a, 'b, Atom: Clone, Elem: Clone> DrainChanges<'a, 'b, Atom, Elem> { impl Drop for DrainChanges<'_, '_, Atom, Elem> { fn drop(&mut self) { + // mark any renumberings as seen + while self.next().is_some() { + } self.tuf .observer_set_rel_pos(self.token, self.tuf.log.len()); } @@ -435,33 +540,12 @@ where } } -#[test] -fn test() { - use imctk_lit::{Lit, Var}; - let l = |n| Var::from_index(n).as_lit(); - let mut tuf = TrackedUnionFind::::new(); - let mut token = tuf.start_observing(); - tuf.union([l(3), !l(4)]); - tuf.union([l(8), l(7)]); - let mut token2 = tuf.start_observing(); - tuf.union([l(4), l(5)]); - for change in tuf.drain_changes(&mut token).cloned() { - println!("{change:?}"); - } - println!("---"); - tuf.union([!l(5), l(6)]); - tuf.make_repr(l(4).var()); - let renumber: IdVec> = - IdVec::from_vec(vec![Some(l(0)), None, None, Some(l(1)), Some(!l(1)), Some(!l(1)), Some(l(1)), Some(l(2)), Some(l(2))]); - let reverse = Renumbering::get_reverse(&renumber, &tuf.union_find); - dbg!(&renumber, &reverse); - tuf.renumber(renumber, reverse); - tuf.union([l(0), l(1)]); - let mut iter = tuf.drain_changes(&mut token); - println!("{:?}", iter.next()); - iter.stop(); - println!("---"); - for change in tuf.drain_changes(&mut token2).cloned() { - println!("{change:?}"); - } +impl ExactSizeIterator for DrainChangesMap<'_, '_, Atom, Elem, F> where + F: FnMut(&Change) -> B +{ +} + +impl FusedIterator for DrainChangesMap<'_, '_, Atom, Elem, F> where + F: FnMut(&Change) -> B +{ } diff --git a/dsf/src/union_find.rs b/dsf/src/union_find.rs index 5036432..ebc54ee 100644 --- a/dsf/src/union_find.rs +++ b/dsf/src/union_find.rs @@ -1,41 +1,53 @@ -#![allow(missing_docs)] -use std::sync::atomic::Ordering; - +//! `UnionFind` efficiently tracks equivalences between variables. +use crate::Element; use atomic::Atomic; -use bytemuck::NoUninit; -use imctk_ids::{id_vec::IdVec, Id}; -use imctk_lit::{Lit, Var}; - -pub trait Element { - fn from_atom(atom: Atom) -> Self; - fn atom(self) -> Atom; - fn apply_pol_of(self, other: Self) -> Self; -} - -impl Element for T { - fn from_atom(atom: T) -> Self { - atom - } - fn atom(self) -> T { - self - } - fn apply_pol_of(self, _other: T) -> Self { - self - } -} +use imctk_ids::{id_vec::IdVec, Id, IdRange}; +use std::sync::atomic::Ordering; -impl Element for Lit { - fn from_atom(atom: Var) -> Self { - atom.as_lit() - } - fn atom(self) -> Var { - self.var() - } - fn apply_pol_of(self, other: Self) -> Self { - self ^ other.pol() - } -} +#[cfg(test)] +#[path = "tests/test_union_find.rs"] +mod test_union_find; +/// `UnionFind` efficiently tracks equivalences between variables. +/// +/// Given "elements" of type `Elem`, this structure keeps track of any known equivalences between these elements. +/// Equivalences are assumed to be *transitive*, i.e. if `x = y` and `y = z`, then `x = z` is assumed to be true, and in fact, +/// automatically discovered by this structure. +/// +/// Unlike a standard union find data structure, this version also keeps track of the *polarity* of elements. +/// For example, you might always have pairs of elements `+x` and `-x` that are exact opposites of each other. +/// If equivalences between `+x` and `-y` are then discovered, the structure also understands that `-x` and `+y` are similarly equivalent. +/// +/// An element without its polarity is called an *atom*. +/// The [`Element`](Element) trait is required on `Elem` so that this structure can relate elements, atoms and polarities. +/// +/// For each set of elements that are equivalent up to a polarity change, this structure keeps track of a *representative*. +/// Each element starts out as its own representative, and if two elements are declared equivalent, the representative of one becomes the representative of both. +/// The method `find` returns the representative for any element. +/// +/// To declare two elements as equivalent, use the `union` or `union_full` methods, see their documentation for details on their use. +/// +/// NB: Since this structure stores atoms in an `IdVec`, the atoms used should ideally be a contiguous set starting at `Atom::MIN_ID`. +/// +/// ## Example ## +/// ``` +/// use imctk_lit::{Var, Lit}; +/// use dsf::UnionFind; +/// +/// let mut union_find: UnionFind = UnionFind::new(); +/// let lit = |n| Var::from_index(n).as_lit(); +/// +/// assert_eq!(union_find.find(lit(4)), lit(4)); +/// +/// union_find.union([lit(3), lit(4)]); +/// assert_eq!(union_find.find(lit(4)), lit(3)); +/// +/// union_find.union([lit(1), !lit(2)]); +/// union_find.union([lit(2), lit(3)]); +/// assert_eq!(union_find.find(lit(1)), lit(1)); +/// assert_eq!(union_find.find(lit(4)), !lit(1)); +/// +/// ``` pub struct UnionFind { parent: IdVec>, } @@ -48,7 +60,7 @@ impl Default for UnionFind { } } -impl Clone for UnionFind { +impl Clone for UnionFind { fn clone(&self) -> Self { let new_parent = self .parent @@ -62,36 +74,63 @@ impl Clone for UnionFind { } } -impl + NoUninit> UnionFind { +impl UnionFind { + /// Constructs an empty `UnionFind`. + /// + /// The returned struct obeys `find(a) == a` for all `a`. pub fn new() -> Self { UnionFind::default() } +} + +impl> UnionFind { + /// Constructs an empty `UnionFind`, with room for `capacity` elements. + pub fn with_capacity(capacity: usize) -> Self { + UnionFind { + parent: IdVec::from_vec(Vec::with_capacity(capacity)), + } + } + /// Clears all equivalences, but retains any allocated memory. + pub fn clear(&mut self) { + self.parent.clear(); + } fn read_parent(&self, atom: Atom) -> Elem { - self.parent - .get(atom) - .map(|p| p.load(Ordering::Relaxed)) - .unwrap_or(Elem::from_atom(atom)) + if let Some(parent_cell) = self.parent.get(atom) { + // This load is allowed to reorder with stores from `update_parent`, see there for details. + parent_cell.load(Ordering::Relaxed) + } else { + Elem::from_atom(atom) + } } + // Important: Only semantically trivial changes are allowed using this method!! + // Specifically, update_parent(atom, parent) should only be called if `parent` is already an ancestor of `atom` + // Otherwise, concurrent calls to `read_parent` (which are explicitly allowed!) could return incorrect results. fn update_parent(&self, atom: Atom, parent: Elem) { - let Some(parent_ref) = self.parent.get(atom) else { + if let Some(parent_cell) = self.parent.get(atom) { + parent_cell.store(parent, Ordering::Relaxed); + } else { + // can only get here if the precondition or a data structure invariant is violated panic!("shouldn't happen: update_parent called with out of bounds argument"); - }; - parent_ref.store(parent, Ordering::Relaxed); + } } + // Unlike `update_parent`, this is safe for arbitrary updates, since it requires &mut self. fn write_parent(&mut self, atom: Atom, parent: Elem) { - if let Some(parent_ref) = self.parent.get(atom) { - parent_ref.store(parent, Ordering::Relaxed); + if let Some(parent_cell) = self.parent.get(atom) { + parent_cell.store(parent, Ordering::Relaxed); } else { debug_assert!(self.parent.next_unused_key() <= atom); while self.parent.next_unused_key() < atom { - self.parent - .push(Atomic::new(Elem::from_atom(self.parent.next_unused_key()))); + let next_elem = Elem::from_atom(self.parent.next_unused_key()); + self.parent.push(Atomic::new(next_elem)); } self.parent.push(Atomic::new(parent)); } } fn find_root(&self, mut elem: Elem) -> Elem { loop { + // If we interleave with a call to `update_parent`, the parent may change + // under our feet, but it's okay because in that case we get an ancestor instead, + // which just skips some iterations of the loop! let parent = self.read_parent(elem.atom()).apply_pol_of(elem); if elem == parent { return elem; @@ -100,220 +139,103 @@ impl + NoUninit> UnionFind { elem = parent; } } + // Worst-case `find` performance is linear. To keep amortised time complexity logarithmic, + // we memoise the result of `find_root` by calling `update_parent` on every element + // we traversed. fn update_root(&self, mut elem: Elem, root: Elem) { + // Loop invariant: `root` is the representative of `elem`. loop { + // Like in `find_root`, this may interleave with `update_root` calls, and we may skip some steps, + // which is okay because the other thread will do the updates instead. let parent = self.read_parent(elem.atom()).apply_pol_of(elem); if parent == root { break; } + // By the loop invariant, this just sets `elem`'s parent to its representative, + // which satisfies the precondition for `update_parent`. Further if two threads end up + // here simultaneously, they will both set to the same representative, + // therefore the change is idempotent. self.update_parent(elem.atom(), root.apply_pol_of(elem)); elem = parent; } } - pub fn find(&self, lit: Elem) -> Elem { - let root = self.find_root(lit); - self.update_root(lit, root); + /// Returns the representative for an element. Elements are equivalent iff they have the same representative. + /// + /// Elements `a` and `b` are equivalent up to a polarity change iff they obey `find(a) = find(b) ^ p` for some polarity `p`. + /// + /// This operation is guaranteed to return `elem` itself for arguments `elem >= lowest_unused_atom()`. + /// + /// The amortised time complexity of this operation is **O**(log N). + pub fn find(&self, elem: Elem) -> Elem { + let root = self.find_root(elem); + self.update_root(elem, root); root } - pub fn union_full(&mut self, lits: [Elem; 2]) -> (bool, [Elem; 2]) { - let [a, b] = lits; + /// Declares two elements to be equivalent. The new representative of both is the representative of the first element. + /// + /// If the elements are already equivalent or cannot be made equivalent (are equivalent up to a sign change), + /// the operation returns `false` without making any changes. Otherwise it returns `true`. + /// + /// In both cases it also returns the original representatives of both arguments. + /// + /// The amortised time complexity of this operation is **O**(log N). + pub fn union_full(&mut self, elems: [Elem; 2]) -> (bool, [Elem; 2]) { + let [a, b] = elems; let ra = self.find(a); let rb = self.find(b); if ra.atom() == rb.atom() { (false, [ra, rb]) } else { + // The first write is only needed to ensure that the parent table actually contains `a` + // and is a no-op otherwise. + self.write_parent(ra.atom(), Elem::from_atom(ra.atom())); self.write_parent(rb.atom(), ra.apply_pol_of(rb)); (true, [ra, rb]) } } - pub fn union(&mut self, lits: [Elem; 2]) -> bool { - self.union_full(lits).0 + /// Declares two elements to be equivalent. The new representative of both is the representative of the first element. + /// + /// If the elements are already equivalent or cannot be made equivalent (are equivalent up to polarity), + /// the operation returns `false` without making any changes. Otherwise it returns `true`. + /// + /// The amortised time complexity of this operation is **O**(log N). + pub fn union(&mut self, elems: [Elem; 2]) -> bool { + self.union_full(elems).0 } + /// Sets `atom` to be its own representative, and updates other representatives to preserve all existing equivalences. + /// + /// The amortised time complexity of this operation is **O**(log N). pub fn make_repr(&mut self, atom: Atom) -> Elem { let root = self.find(Elem::from_atom(atom)); self.write_parent(atom, Elem::from_atom(atom)); self.write_parent(root.atom(), Elem::from_atom(atom).apply_pol_of(root)); root } + /// Returns the lowest `Atom` value for which no equivalences are known. + /// + /// It is guaranteed that `find(a) == a` if `a >= lowest_unused_atom`, but the converse may not hold. pub fn lowest_unused_atom(&self) -> Atom { self.parent.next_unused_key() } -} - -#[cfg(test)] -#[allow(dead_code)] -mod tests { - use super::*; - use imctk_ids::id_set_seq::IdSetSeq; - use rand::prelude::*; - use std::collections::{HashSet, VecDeque}; - - #[derive(Default)] - struct CheckedUnionFind { - dut: UnionFind, - equivs: IdSetSeq, - } - - impl + NoUninit> UnionFind { - fn debug_print_tree( - children: &IdVec>, - atom: Atom, - prefix: &str, - self_char: &str, - further_char: &str, - pol: bool, - ) { - println!( - "{prefix}{self_char}{}{:?}", - if pol { "!" } else { "" }, - atom - ); - let my_children = children.get(atom).unwrap(); - for (index, &child) in my_children.iter().enumerate() { - let last = index == my_children.len() - 1; - let self_char = if last { "└" } else { "├" }; - let next_further_char = if last { " " } else { "│" }; - Self::debug_print_tree( - children, - child.atom(), - &(prefix.to_string() + further_char), - self_char, - next_further_char, - pol ^ (child != Elem::from_atom(child.atom())), - ); - } - } - fn debug_print(&self) { - let mut children: IdVec> = Default::default(); - for atom in self.parent.keys() { - let parent = self.read_parent(atom); - children.grow_for_key(atom); - if atom != parent.atom() { - children - .grow_for_key(parent.atom()) - .push(Elem::from_atom(atom).apply_pol_of(parent)); - } else { - assert!(Elem::from_atom(atom) == parent); - } - } - for atom in self.parent.keys() { - if atom == self.read_parent(atom).atom() { - Self::debug_print_tree(&children, atom, "", "", " ", false); - } - } - } - } - #[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] - enum VarRel { - Equiv, - AntiEquiv, - NotEquiv, - } - - impl + NoUninit> CheckedUnionFind { - fn new() -> Self { - CheckedUnionFind { - dut: Default::default(), - equivs: Default::default(), - } - } - fn ref_equal(&mut self, start: Elem, goal: Elem) -> VarRel { - let mut seen: HashSet = Default::default(); - let mut queue: VecDeque = [start].into(); - while let Some(place) = queue.pop_front() { - if place.atom() == goal.atom() { - if place == goal { - return VarRel::Equiv; - } else { - return VarRel::AntiEquiv; - } - } - seen.insert(place.atom()); - for &next in self.equivs.grow_for(place.atom()).iter() { - if !seen.contains(&next.atom()) { - queue.push_back(next.apply_pol_of(place)); - } - } - } - VarRel::NotEquiv - } - fn find(&mut self, lit: Elem) -> Elem { - let out = self.dut.find(lit); - assert!(self.ref_equal(lit, out) == VarRel::Equiv); - out - } - fn union_full(&mut self, lits: [Elem; 2]) -> (bool, [Elem; 2]) { - let (ok, [ra, rb]) = self.dut.union_full(lits); - assert_eq!(self.ref_equal(lits[0], ra), VarRel::Equiv); - assert_eq!(self.ref_equal(lits[1], rb), VarRel::Equiv); - assert_eq!(ok, self.ref_equal(lits[0], lits[1]) == VarRel::NotEquiv); - assert_eq!(self.dut.find_root(lits[0]), ra); - if ok { - assert_eq!(self.dut.find_root(lits[1]), ra); - self.equivs - .grow_for(lits[0].atom()) - .insert(lits[1].apply_pol_of(lits[0])); - self.equivs - .grow_for(lits[1].atom()) - .insert(lits[0].apply_pol_of(lits[1])); - } else { - assert_eq!(self.dut.find_root(lits[1]).atom(), ra.atom()); - } - (ok, [ra, rb]) - } - fn union(&mut self, lits: [Elem; 2]) -> bool { - self.union_full(lits).0 - } - fn make_repr(&mut self, lit: Atom) { - self.dut.make_repr(lit); - assert_eq!( - self.dut.find_root(Elem::from_atom(lit)), - Elem::from_atom(lit) - ); - self.check(); - } - fn check(&mut self) { - for atom in self.dut.parent.keys() { - let parent = self.dut.read_parent(atom); - assert_eq!(self.ref_equal(Elem::from_atom(atom), parent), VarRel::Equiv); - let root = self.dut.find_root(Elem::from_atom(atom)); - for &child in self.equivs.grow_for(atom).iter() { - assert_eq!(root, self.dut.find_root(child)); - } - } - } + /// Returns an iterator that yields all tracked atoms and their representatives. + pub fn iter(&self) -> impl '_ + Iterator { + IdRange::from(Atom::MIN_ID..self.lowest_unused_atom()) + .iter() + .map(|atom| (atom, self.find(Elem::from_atom(atom)))) } +} - #[test] - fn test() { - let mut u: CheckedUnionFind = CheckedUnionFind::new(); - let mut rng = rand_pcg::Pcg64::seed_from_u64(25); - let max_var = 2000; - for i in 0..2000 { - match rng.gen_range(0..10) { - 0..=4 => { - let a = Lit::from_code(rng.gen_range(0..=2 * max_var + 1)); - let b = Lit::from_code(rng.gen_range(0..=2 * max_var + 1)); - let result = u.union_full([a, b]); - println!("union({a}, {b}) = {result:?}"); - } - 5..=7 => { - let a = Lit::from_code(rng.gen_range(0..=2 * max_var + 1)); - let result = u.find(a); - println!("find({a}) = {result}"); - } - 8 => { - u.check(); - } - 9 => { - let a = Var::from_index(rng.gen_range(0..=max_var)); - u.make_repr(a); - println!("make_repr({a})"); - } - _ => {} +impl> std::fmt::Debug for UnionFind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // prints non-trivial sets of equivalent elements, always printing the representative first + let mut sets = std::collections::HashMap::>::new(); + for (atom, repr) in self.iter() { + if Elem::from_atom(atom) != repr { + sets.entry(repr.atom()) + .or_insert_with(|| vec![Elem::from_atom(repr.atom())]) + .push(Elem::from_atom(atom).apply_pol_of(repr)); } } - u.check(); - //u.dut.debug_print(); + f.debug_set().entries(sets.values()).finish() } } diff --git a/ids/src/id_alloc.rs b/ids/src/id_alloc.rs new file mode 100644 index 0000000..630ed8f --- /dev/null +++ b/ids/src/id_alloc.rs @@ -0,0 +1,54 @@ +//! An allocator for IDs that allows concurrent access from multiple threads. +use std::sync::atomic::Ordering::Relaxed; +use std::{marker::PhantomData, sync::atomic::AtomicUsize}; + +use crate::{Id, IdRange}; + +/// An allocator for IDs that allows concurrent access from multiple threads. +pub struct IdAlloc { + counter: AtomicUsize, + _phantom: PhantomData, +} + +impl Default for IdAlloc { + fn default() -> Self { + Self::new() + } +} + +/// `IdAllocError` indicates that there are not enough IDs remaining. +#[derive(Clone, Copy, Debug)] +pub struct IdAllocError; + +impl IdAlloc { + /// Constructs a new ID allocator. + pub const fn new() -> Self { + Self { + counter: AtomicUsize::new(0), + _phantom: PhantomData, + } + } + fn alloc_indices(&self, n: usize) -> Result { + self.counter + .fetch_update(Relaxed, Relaxed, |current_id| { + current_id + .checked_add(n) + .filter(|&index| index <= T::MAX_ID_INDEX.saturating_add(1)) + }) + .map_err(|_| IdAllocError) + } + /// Allocates a single ID. + pub fn alloc(&self) -> Result { + self.alloc_indices(1).map(|index| { + // SAFETY: the precondition was checked by `alloc_indices` + unsafe { T::from_id_index_unchecked(index) } + }) + } + /// Allocates a contiguous range of the specified size. + pub fn alloc_range(&self, n: usize) -> Result, IdAllocError> { + self.alloc_indices(n).map(|start| { + // SAFETY: the precondition was checked by `alloc_indices` + unsafe { IdRange::from_index_range_unchecked(start..start + n) } + }) + } +} diff --git a/ids/src/lib.rs b/ids/src/lib.rs index a65669b..da26bd5 100644 --- a/ids/src/lib.rs +++ b/ids/src/lib.rs @@ -16,6 +16,7 @@ mod id; mod id_range; pub mod id_vec; pub mod indexed_id_vec; +pub mod id_alloc; pub mod id_set_seq; @@ -32,6 +33,8 @@ pub use id::{ConstIdFromIdIndex, GenericId, Id, Id16, Id32, Id64, Id8, IdSize}; pub use id_range::IdRange; +pub use id_alloc::IdAlloc; + // re-export this so that others can use it without depending on bytemuck explicitly // in particular needed for #[derive(Id)] pub use bytemuck::NoUninit; \ No newline at end of file