diff --git a/Cargo.toml b/Cargo.toml index 7e12c97..9345f0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,9 @@ name = "shepherd" version = "0.1.0" edition = "2021" +default-run = "shepherd" [dependencies] -log = "0.4.27" env_logger = "0.11.8" regex = "1" clap = { version = "4.5.35", features = ["derive"] } @@ -26,3 +26,6 @@ itertools = "0.14.0" name = "schaeppert" path = "src/iterative.rs" +[dependencies.log] +version = "0.4.27" +features = ["release_max_level_info"] diff --git a/latex/examples.tex b/latex/examples.tex index 10c2837..f223d77 100644 --- a/latex/examples.tex +++ b/latex/examples.tex @@ -53,4 +53,10 @@ \subsection{Bottleneck 2.} File {\tt bottleneck-2.tikz} \input{../examples/bottleneck-2.tikz} + +\subsection{Bug 12} + +File {\tt bug12.tikz} + +\input{../examples/bug12.tikz} \end{document} diff --git a/src/cli.rs b/src/cli.rs index 935968b..6f7c960 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,9 +1,9 @@ //! This module defines the command line interface (CLI) for the application. +use crate::nfa; +use crate::solver; use clap::{Parser, ValueEnum}; use std::path::PathBuf; -use crate::solver; -use crate::nfa; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] pub enum OutputFormat { @@ -31,7 +31,7 @@ pub struct Args { short = 'v', long = "verbose", action = clap::ArgAction::Count, - help = "Increase verbosity level" + help = "Increase verbosity level. Default is warn only, -v is info, -vv is debug, -vvv is trace" )] pub verbosity: u8, @@ -73,7 +73,7 @@ pub struct Args { long, value_enum, default_value = "strategy", - help = "Solver output specification." + help = "Solver output specification. Strategy will always compute the maximal winning strategy. Yes-no may be faster when the answer is positive, it uses a heuristic trying to find a simple strategy, which is winning but might not be maximal." )] pub solver_output: solver::SolverOutput, } diff --git a/src/downset.rs b/src/downset.rs index c811c46..08cb306 100644 --- a/src/downset.rs +++ b/src/downset.rs @@ -1,15 +1,11 @@ use crate::coef::{coef, Coef, C0, OMEGA}; use crate::ideal::Ideal; -use crate::memoizer::Memoizer; use crate::partitions; use cached::proc_macro::cached; use itertools::Itertools; -use log::debug; -use once_cell::sync::Lazy; -use rayon::prelude::*; +use log::{debug, trace, warn}; use std::collections::VecDeque; use std::fmt; -use std::sync::Mutex; use std::{collections::HashSet, vec::Vec}; /* @@ -36,38 +32,38 @@ impl PartialEq for DownSet { } type CoefsCollection = Vec>; -type Herd = Vec; -type CoefsCollectionMemoizer = Memoizer Herd>; -static POSSIBLE_COEFS_CACHE: Lazy> = Lazy::new(|| { - Mutex::new(Memoizer::new(|possible_coefs| { - compute_possible_coefs(possible_coefs) - .map(Ideal::from_vec) - .collect() - })) -}); - -fn compute_possible_coefs(possible_coefs: &CoefsCollection) -> impl Iterator> { - possible_coefs + +/** + * every vector comes in order omega / 0 / c+1 / 2 / 1 + */ +fn expand_finite_downward_closure( + maximal_finite_coef: &Vec, + is_omega_sometimes_possible: &Vec, + is_omega_always_possible: &Vec, +) -> CoefsCollection { + trace!( + "expand_finite_downward_closure maximal_finite_coef {:?} is_omega_sometimes_possible {:?} is_omega_always_possible {:?})", + maximal_finite_coef, + is_omega_sometimes_possible, + is_omega_always_possible + ); + assert!(maximal_finite_coef.len() == is_omega_sometimes_possible.len()); + assert!(maximal_finite_coef.len() == is_omega_always_possible.len()); + maximal_finite_coef .iter() - .map(|v| { - let coef = v - .iter() - .filter_map(|&x| match x { - OMEGA => None, - Coef::Value(c) => Some(c), - }) - .next(); - let is_omega = v.contains(&OMEGA); - match (is_omega, coef) { - (false, None) => vec![C0], - (true, None) => vec![OMEGA], - (false, Some(c)) => (0..c + 1).map(Coef::Value).collect(), - (true, Some(c)) => std::iter::once(OMEGA) - .chain((0..c + 1).map(Coef::Value)) + .enumerate() + .map(|(i, &coef)| { + let is_omega_sometimes = is_omega_sometimes_possible[i]; + let is_omega_always = is_omega_always_possible[i]; + match (is_omega_always, is_omega_sometimes, coef) { + (true, _, _) => vec![OMEGA], + (false, true, _) => vec![C0, OMEGA], + (false, false, c) => std::iter::once(C0) + .chain((1..c + 1).map(Coef::Value).rev()) .collect(), } }) - .multi_cartesian_product() + .collect() } impl DownSet { @@ -245,16 +241,22 @@ impl DownSet { //compute for every i whether omega should be allowed at i, //this is the case iff there exists a ideal in the downward-closed set such that //on that coordinate the non-empty set of successors all lead to omega - let is_omega_possible = (0..dim) + let is_omega_sometimes_possible = (0..dim) .map(|i| { let succ = edges.get_successors(i); !succ.is_empty() && self.0.iter().any(|ideal| ideal.all_omega(&succ)) }) .collect::>(); + let is_omega_always_possible = (0..dim) + .map(|i| { + let succ = edges.get_successors(i); + !succ.is_empty() && self.0.iter().all(|ideal| ideal.all_omega(&succ)) + }) + .collect::>(); //compute for every j the maximal finite coef appearing at index j, if exists //omega are turned to 1 - let max_finite_coordsj: Vec = (0..dim) + let max_finite_coords_post: Vec = (0..dim) .map(|j: usize| { self.0 .iter() @@ -268,55 +270,143 @@ impl DownSet { }) .collect::>(); - let max_finite_coordsi = (0..dim) + let max_finite_coords_pre = (0..dim) .map(|i| { { edges .get_successors(i) .iter() - .map(|&j| std::cmp::min(maximal_finite_coordinate, max_finite_coordsj[j])) - .max() + .map(|&j| { + std::cmp::min(maximal_finite_coordinate, max_finite_coords_post[j]) + }) + .min() .unwrap_or(0) } }) .collect::>(); - //println!("preimage of\n{}\n by\n{}\n", self, edges); + trace!("preimage of\n{}\n by\n{}\n", self, edges); + trace!("max_finite_coords: {:?}\n", max_finite_coords_pre); + trace!( + "is_omega_sometimes_possible: {:?}\n", + is_omega_sometimes_possible + ); + trace!("is_omega_always_possible: {:?}\n", is_omega_always_possible); - let possible_coefs = (0..dim) - .map(|i| { - match ( - max_finite_coordsi.get(i).unwrap(), - is_omega_possible.get(i).unwrap(), - ) { - (0, false) => vec![Coef::Value(0)], - (0, true) => vec![OMEGA], - (&c, false) => vec![Coef::Value(c)], - (&c, true) => vec![OMEGA, Coef::Value(c)], - } - }) - .collect::>(); - //println!("max_finite_coords: {:?}\n", max_finite_coordsi); - //println!("is_omega_possible: {:?}\n", is_omega_possible); - //println!("possible_coefs: {:?}\n", possible_coefs); + let all_possible_coefs: CoefsCollection = expand_finite_downward_closure( + &max_finite_coords_pre, + &is_omega_sometimes_possible, + &is_omega_always_possible, + ); + + let max_iteration_nb = all_possible_coefs.iter().fold(1, |acc, l| acc * l.len()); + trace!("max_iteration_nb: {:?}\n", max_iteration_nb); + if max_iteration_nb > 10_000_000 { + warn!("iterating over a potentially very large number of possible ideals (up to {}), will possibly never terminate", max_iteration_nb); + } else { + debug!("iterating over up to {} possible ideals", max_iteration_nb); + } let mut result = DownSet::new(); - let candidates = POSSIBLE_COEFS_CACHE.lock().unwrap().get(possible_coefs); - candidates - .par_iter() - .filter(|&candidate| { - self.is_safe_with_roundup(candidate, edges, maximal_finite_coordinate) - }) - .collect::>() - .iter() - .for_each(|c| { - result.insert(c); - }); + let mut iterator: Vec = DownSet::get_initial_iterator(&all_possible_coefs); + let mut is_not_over = true; + while is_not_over { + let coordinates = iterator + .iter() + .enumerate() + .map(|(i, &j)| all_possible_coefs[i][j]) + .collect(); + let candidate = Ideal::from_vec(coordinates); + trace!("checking candidate {} for safe preimage", candidate); + let is_safe = self.is_safe_with_roundup(&candidate, edges, maximal_finite_coordinate); + if is_safe { + trace!("{} is safe", candidate); + result.insert(&candidate); + } else { + trace!("{} is unsafe", candidate); + } + is_not_over = DownSet::get_next_iterator(&mut iterator, &all_possible_coefs, is_safe); + } + + trace!("minimizing result"); result.minimize(); - //println!("result {}\n", result); + trace!("result {}\n", result); result } + fn get_initial_iterator(all_possible_coefs: &CoefsCollection) -> Vec { + assert!(all_possible_coefs.iter().all(|l| !l.is_empty())); + all_possible_coefs.iter().map(|_l| 0).collect() + } + fn get_next_iterator( + iterator: &mut [usize], + all_possible_coefs: &CoefsCollection, + all_below_current_are_safe: bool, + ) -> bool { + assert!(iterator.len() == all_possible_coefs.len()); + if all_below_current_are_safe { + //l'idéal actuel est gagant donc tous les idéaux plus petits également + //on va chercherun itérateur qui n'est aps plus petit + DownSet::get_next_not_below_current(iterator, all_possible_coefs) + } else { + let mut non_zero = false; + for i in (0..iterator.len()).rev() { + if all_possible_coefs[i].len() == 1 { + continue; //only one coef at this index + } + non_zero |= iterator[i] > 0; + if (iterator[i] == 0 && non_zero) + || (0 < iterator[i] && iterator[i] < all_possible_coefs[i].len() - 1) + { + iterator[i] += 1; + return true; + } + iterator[i] = 0; + } + false + } + } + fn get_next_not_below_current( + iterator: &mut [usize], + all_possible_coefs: &CoefsCollection, + ) -> bool { + assert!(iterator.len() == all_possible_coefs.len()); + for i in (0..iterator.len()).rev() { + if all_possible_coefs[i].len() == 1 { + continue; //only one coef at this index + } + if iterator[i] == 1 { + //on a déjà mis le 1, on ne peut pas incrémenter + continue; + } + if iterator[i] == 0 { + //il suffit de passer à 1 et resetter à droite + iterator[i] = 1; + iterator.iter_mut().skip(i + 1).for_each(|x| *x = 0); + return true; + } + iterator[i] -= 1; + iterator.iter_mut().skip(i + 1).for_each(|x| *x = 0); + //on va incrémenter à gauche de i, dès que possible + let mut j = i; + while j > 0 { + j -= 1; + let itj1 = iterator[j]; + let len1 = all_possible_coefs[j].len() - 1; + if len1 >= 1 && itj1 < len1 { + if itj1 == 0 { + iterator[j] = 1; //on met celui-là au max + iterator.iter_mut().skip(j + 1).for_each(|x| *x = 0); + return true; + } else { + iterator[j] += 1; + return true; + } + } + } + } + false + } /* naive exponential impl of get_intersection_with_safe_ideal*/ fn safe_post( ideal: &Ideal, @@ -324,36 +414,37 @@ impl DownSet { safe: &DownSet, maximal_finite_value: coef, ) -> DownSet { - /* - println!( + trace!( "get_intersection_with_safe_ideal\nideal: {}\nsafe_target\n{}\nedges\n{}", - ideal, safe_target, edges - ); */ + ideal, + safe, + edges + ); let mut result = DownSet::new(); let mut to_process: VecDeque = vec![ideal.clone()].into_iter().collect(); let mut processed = HashSet::::new(); while !to_process.is_empty() { let flow = to_process.pop_front().unwrap(); - //print!("Processing {}...", flow); + trace!("Processing {}...", flow); if result.contains(&flow) { - //println!("...already included"); + trace!("...already included"); continue; } if processed.contains(&flow) { - //println!("...already processed"); + trace!("...already processed"); continue; } processed.insert(flow.clone()); if Self::is_safe(ideal, edges, safe, ideal.dimension(), maximal_finite_value) { - //println!("...safe"); + trace!("...safe"); result.insert(ideal); } else { - //println!("...unsafe"); + trace!("...unsafe"); flow.iter().enumerate().for_each(|(i, &ci)| { if ci != C0 { let smaller = flow.clone_and_decrease(i, maximal_finite_value); if !processed.contains(&smaller) { - //println!("adding smaller {} to queue", smaller); + trace!("adding smaller {} to queue", smaller); to_process.push_back(smaller); } } @@ -374,15 +465,15 @@ impl DownSet { maximal_finite_coordinate: coef, ) { if accumulator.contains(candidate) { - //println!("{} already in ideal", candidate); + trace!("{} already in ideal", candidate); return; } if self.is_safe_with_roundup(candidate, edges, maximal_finite_coordinate) { - //println!("{} inserted", candidate); + trace!("{} inserted", candidate); accumulator.insert(candidate); return; } - //println!("{} refined", candidate); + trace!("{} refined", candidate); let mut candidate_copy = candidate.clone(); for i in 0..candidate.dimension() { let ci = candidate.get(i); @@ -453,7 +544,7 @@ impl DownSet { } let image: DownSet = Self::get_image(dim, candidate, edges, maximal_finite_coordinate); - //println!("image\n{}", &image); + trace!("image\n{}", &image); let answer = image.ideals().all(|x| self.contains(x)); answer } @@ -551,7 +642,7 @@ impl DownSet { #[cached] fn get_choices(dim: usize, value: Coef, successors: Vec) -> Vec { - //println!("get_choices({}, {:?}, {:?})", dim, value, successors); + trace!("get_choices({}, {:?}, {:?})", dim, value, successors); //assert!(value == OMEGA || value <= Coef::Value(dim as coef)); match value { C0 => vec![Ideal::new(dim, C0)], @@ -593,7 +684,7 @@ impl fmt::Display for DownSet { mod test { use super::*; - use crate::coef::{C0, C1, C2, OMEGA}; + use crate::coef::{C0, C1, C2, C3, OMEGA}; #[test] fn is_in_ideal() { @@ -869,4 +960,23 @@ mod test { let pre_image0 = downset0.safe_pre_image(&edges, dim as coef); assert_eq!(pre_image0, DownSet::from_vecs(&[&[C2, C0, C0, C0, C0]])); } + + #[test] + fn expand_finite_downward_closure() { + use crate::downset::expand_finite_downward_closure; + let expanded = expand_finite_downward_closure( + &vec![0, 3, 1, 0], + &vec![true, false, false, true], + &vec![true, false, false, false], + ); + assert_eq!( + expanded, + vec![ + vec![OMEGA], + vec![C0, C3, C2, C1], + vec![C0, C1], + vec![C0, OMEGA], + ] + ); + } } diff --git a/src/main.rs b/src/main.rs index c0bd08f..dc33c94 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,11 @@ use clap::Parser; +use log::info; use std::fs::File; use std::io; use std::io::Write; -use log::info; -use shepherd::solver; use shepherd::nfa; +use shepherd::solver; mod cli; mod logging; diff --git a/src/strategy.rs b/src/strategy.rs index 3b0e7f0..520af5a 100644 --- a/src/strategy.rs +++ b/src/strategy.rs @@ -3,6 +3,7 @@ use crate::downset::DownSet; use crate::graph::Graph; use crate::ideal::Ideal; use crate::nfa; +use log::trace; use std::collections::HashMap; use std::fmt; @@ -40,8 +41,18 @@ impl Strategy { // print!("."); //io::stdout().flush().unwrap(); let edges = edges_per_letter.get(a).unwrap(); + trace!( + "Restricting strategy for letter '{}' with downset {}", + a, + downset + ); let safe_pre_image = safe.safe_pre_image(edges, maximal_finite_value); result |= downset.restrict_to(&safe_pre_image); + trace!( + "After restriction, downset for letter '{}' is {}", + a, + downset + ); } result } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index f243259..4d1ff74 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,8 +1,8 @@ -use shepherd::nfa; -use shepherd::solver; use shepherd::coef::{C0, C1, C2, OMEGA}; use shepherd::downset::DownSet; use shepherd::ideal::Ideal; +use shepherd::nfa; +use shepherd::solver; const EXAMPLE1: &str = include_str!("../examples/bottleneck-1-ab.tikz"); const EXAMPLE1_COMPLETE: &str = include_str!("../examples/bottleneck-1-ab-complete.tikz"); @@ -138,4 +138,6 @@ fn test_bug12() { .unwrap(); println!("{}", downsetb); assert!(downsetb.contains(&Ideal::from_vec(vec![C2, C0, C0, C0, C0, C0, C0, C0]))); + assert!(downsetb.contains(&Ideal::from_vec(vec![C0, C1, C1, C0, C0, C0, C0, C0]))); + assert!(downsetb.contains(&Ideal::from_vec(vec![C0, C0, C0, C0, C0, C0, C0, OMEGA]))); }