diff --git a/Cargo.lock b/Cargo.lock index adc1040..4d1e5b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,7 @@ dependencies = [ [[package]] name = "didp-yaml" -version = "0.7.1" +version = "0.7.2" dependencies = [ "approx", "dypdl", @@ -124,7 +124,7 @@ dependencies = [ [[package]] name = "didppy" -version = "0.7.1" +version = "0.7.2" dependencies = [ "dypdl", "dypdl-heuristic-search", @@ -135,7 +135,7 @@ dependencies = [ [[package]] name = "dypdl" -version = "0.7.1" +version = "0.7.2" dependencies = [ "approx", "fixedbitset", @@ -146,7 +146,7 @@ dependencies = [ [[package]] name = "dypdl-heuristic-search" -version = "0.7.1" +version = "0.7.2" dependencies = [ "bus", "crossbeam-channel", diff --git a/didp-yaml/Cargo.toml b/didp-yaml/Cargo.toml index 278e60c..5eda359 100644 --- a/didp-yaml/Cargo.toml +++ b/didp-yaml/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "didp-yaml" -version = "0.7.1" +version = "0.7.2" edition = "2021" rust-version = "1.65" description = "YAML interface for Dynamic Programming Description Language (DyPDL) and DyPDL solvers." @@ -12,8 +12,8 @@ repository = "https://github.com/domain-independent-dp/didp-rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dypdl = { path = "../dypdl", version = "0.7.1" } -dypdl-heuristic-search = { path = "../dypdl-heuristic-search", version = "0.7.1" } +dypdl = { path = "../dypdl", version = "0.7.2" } +dypdl-heuristic-search = { path = "../dypdl-heuristic-search", version = "0.7.2" } rustc-hash = "1.1" yaml-rust = "0.4" linked-hash-map = "0.5" diff --git a/didp-yaml/docs/solver-guide.md b/didp-yaml/docs/solver-guide.md index 5679d87..6eb8449 100644 --- a/didp-yaml/docs/solver-guide.md +++ b/didp-yaml/docs/solver-guide.md @@ -166,7 +166,7 @@ config: - default: `2023` - `has_negative_cost`: whether the cost of a transition can be negative. - default: `false` -- `use_cost_weight`: use weighted sampling biased by costs to select a start of a partial path. This is not activated when `has_negative_cost` is `true`. +- `use_cost_weight`: use weighted sampling biased by costs to select a start of a partial path. - default: `false` - `no_bandit`: do not use bandit-based sampling to select the depth of a partial path. - default: `false` diff --git a/didppy/Cargo.toml b/didppy/Cargo.toml index 88aa999..e886daf 100644 --- a/didppy/Cargo.toml +++ b/didppy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "didppy" -version = "0.7.1" +version = "0.7.2" edition = "2021" rust-version = "1.65" description = "Python interface for Dynamic Programming Description Language (DyPDL) and DyPDL solvers." @@ -15,8 +15,8 @@ name = "didppy" crate-type = ["cdylib"] [dependencies] -dypdl = { path = "../dypdl", version = "0.7.1" } -dypdl-heuristic-search = { path = "../dypdl-heuristic-search", version = "0.7.1" } +dypdl = { path = "../dypdl", version = "0.7.2" } +dypdl-heuristic-search = { path = "../dypdl-heuristic-search", version = "0.7.2" } rustc-hash = "1.1" [dependencies.pyo3] diff --git a/didppy/docs/papers.rst b/didppy/docs/papers.rst index 51f474b..5f7884b 100644 --- a/didppy/docs/papers.rst +++ b/didppy/docs/papers.rst @@ -9,4 +9,6 @@ DIDP Papers * This paper introduces Large Neighborhood Beam Search (LNBS). * Ryo Kuroiwa and J. Christopher Beck. `Parallel Beam Search Algorithms for Domain-Independent Dynamic Programming `_. In Proceedings of the 38th Annual AAAI Conference on Artificial Intelligence (AAAI). 2024. * This paper parallelizes :class:`~didppy.CABS`. +* Ryo Kuroiwa and J. Christopher Beck. `Domain-Independent Dynamic Programming `_. arXiv. 2024. + * This paper provides formal definitions of the modeling language and solvers for DIDP. It also introduces DIDP models for the orienteering problem with time windows and the multi-dimensional knapsack problem. diff --git a/didppy/docs/solver-selection.rst b/didppy/docs/solver-selection.rst index 220d2e7..55eb4d9 100644 --- a/didppy/docs/solver-selection.rst +++ b/didppy/docs/solver-selection.rst @@ -30,8 +30,9 @@ However, these alternatives consume more memory than :class:`didppy.CABS`, so if The experimental comparison of :class:`~didppy.CAASDy` and the anytime solvers is provided in :cite:t:`DIDPAnytime`. If the time to prove optimality is not very important, and you want to find a good solution quickly, :class:`~didppy.LNBS` may be also useful. -It is slower than :class:`~didppy.CABS` to prove the optimality, but it tends to find a better solution quickly in routing and scheduling problems such as :ref:`TSPTW ` and :ref:`talent scheduling `. -In contrast, :class:`~didppy.CABS` is better in :ref:`MOSP ` for example. +It is slower than :class:`~didppy.CABS` to prove the optimality, but it tends to find a better solution quickly when the dual bound functions are not tight. +For example, :class:`~didppy.LNBS` is better than :class:`~didppy.CABS` with the DIDP models for :ref:`TSPTW ` and :ref:`talent scheduling `. +In contrast, :class:`~didppy.CABS` is better with the DIDP model for :ref:`MOSP ` for example. Layer-by-Layer Search diff --git a/didppy/src/heuristic_search_solver/lnbs.rs b/didppy/src/heuristic_search_solver/lnbs.rs index a18fb8f..ce8ee82 100644 --- a/didppy/src/heuristic_search_solver/lnbs.rs +++ b/didppy/src/heuristic_search_solver/lnbs.rs @@ -18,7 +18,7 @@ use std::sync::Arc; /// This performs LNBS using the dual bound as the heuristic function. /// LNBS is complete, i.e., eventually finds the optimal solution, but is designed to find a good solution rather than proving the optimality. /// If you want to prove the optimality, :class:`didppy.CABS` or :class:`didppy.CAASDy` might be better. -/// LNBS typically performs well in routing and scheduling problems, where solution costs are diverse. +/// LNBS typically performs better when the dual bound functions are not tight. /// /// To apply this solver, the cost must be computed in the form of :code:`x + state_cost`, :code:`x * state_cost`, :code:`didppy.max(x, state_cost)`, /// or :code:`didppy.min(x, state_cost)` where, :code:`state_cost` is either of :meth:`IntExpr.state_cost()` and :meth:`FloatExpr.state_cost()`, @@ -63,7 +63,6 @@ use std::sync::Arc; /// Whether the cost of a transition can be negative. /// use_cost_weight: bool, default: False /// Use weighted sampling biased by costs to select a start of a partial path. -/// This is not activated when :code:`has_negative_cost` is :code:`True`. /// no_bandit: bool, default: False /// Do not use bandit-based sampling to select the depth of a partial path. /// no_transition_mutex: bool, default: False diff --git a/dypdl-heuristic-search/Cargo.toml b/dypdl-heuristic-search/Cargo.toml index 89fac6f..88c3426 100644 --- a/dypdl-heuristic-search/Cargo.toml +++ b/dypdl-heuristic-search/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dypdl-heuristic-search" -version = "0.7.1" +version = "0.7.2" edition = "2021" rust-version = "1.65" description = "Heuristic search solvers for Dynamic Programming Description Language (DyPDL)." @@ -13,7 +13,7 @@ repository = "https://github.com/domain-independent-dp/didp-rs" [dependencies] ordered-float = "3.9" -dypdl = { path = "../dypdl", version = "0.7.1" } +dypdl = { path = "../dypdl", version = "0.7.2" } rustc-hash = "1.1" dashmap = "5.5" rayon = "1.8" diff --git a/dypdl-heuristic-search/src/parallel_search_algorithm/data_structure/termination_detector.rs b/dypdl-heuristic-search/src/parallel_search_algorithm/data_structure/termination_detector.rs index 3bffb1a..1a2771a 100644 --- a/dypdl-heuristic-search/src/parallel_search_algorithm/data_structure/termination_detector.rs +++ b/dypdl-heuristic-search/src/parallel_search_algorithm/data_structure/termination_detector.rs @@ -41,7 +41,7 @@ impl TerminationDetector { /// Notifies that a message has been received. pub fn notify_received(&mut self, tstamp: usize) { - self.clock = max(tstamp, self.clock); + self.tmax = max(tstamp, self.tmax); self.count -= 1; } diff --git a/dypdl-heuristic-search/src/search_algorithm/lnbs.rs b/dypdl-heuristic-search/src/search_algorithm/lnbs.rs index d331714..8123ee7 100644 --- a/dypdl-heuristic-search/src/search_algorithm/lnbs.rs +++ b/dypdl-heuristic-search/src/search_algorithm/lnbs.rs @@ -298,7 +298,7 @@ where } } - //// Search for the next solution, returning the solution using `TransitionWithId`. + /// Search for the next solution, returning the solution using `TransitionWithId`. pub fn search_inner(&mut self) -> (Solution>, bool) { if self.input.solution.is_terminated() || self.input.solution.cost.is_none() { return (self.input.solution.clone(), true); @@ -443,7 +443,8 @@ where let current_cost = self.input.solution.cost.unwrap(); let reward = (cost - current_cost).to_continuous().abs() - / current_cost.to_continuous().abs(); + / cmp::max(cost.abs(), current_cost.abs()).to_continuous(); + let reward = if reward > 1.0 { 1.0 } else { reward }; let time = (self.time_keeper.elapsed_time() - time_start) / self.time_limit; self.update_bandit(arm, reward, time); @@ -573,7 +574,7 @@ where } fn select_start(&mut self, costs: &[T], depth: usize) -> Option<(usize, usize)> { - let search_non_positive = self.has_negative_cost + let not_cost_algebraic_minimization = self.has_negative_cost || self.input.successor_generator.model.reduce_function == ReduceFunction::Max; let (weights, starts): (Vec<_>, Vec<_>) = std::iter::once(T::zero()) @@ -586,9 +587,9 @@ where .entry((start, depth)) .or_insert((self.initial_beam_size, false)); - if entry.1 || (!search_non_positive && after <= before) { + if entry.1 || (!not_cost_algebraic_minimization && after <= before) { None - } else if !search_non_positive && self.use_cost_weight { + } else if self.use_cost_weight { Some((after - before, (start, entry.0))) } else { Some((T::one(), (start, entry.0))) @@ -600,17 +601,63 @@ where return None; } - let mut weights = weights - .iter() - .map(|v| v.to_continuous()) - .collect::>(); + let weights = if not_cost_algebraic_minimization && self.use_cost_weight { + if self.input.successor_generator.model.reduce_function == ReduceFunction::Max { + let max_weight = weights.iter().copied().max().unwrap(); + + if let Some(second_max) = weights.iter().copied().filter(|v| *v < max_weight).max() + { + weights + .into_iter() + .zip(starts.iter()) + .map(|(v, (_, beam_size))| { + (max_weight - cmp::min(v, second_max)).to_continuous() + / *beam_size as f64 + }) + .collect() + } else { + weights + .into_iter() + .zip(starts.iter()) + .map(|(_, (_, beam_size))| 1.0 / *beam_size as f64) + .collect() + } + } else { + let min_weight = weights.iter().copied().min().unwrap(); + + if let Some(second_min) = weights.iter().copied().filter(|v| *v > min_weight).min() + { + weights + .into_iter() + .zip(starts.iter()) + .map(|(v, (_, beam_size))| { + (cmp::max(v, second_min) - min_weight).to_continuous() + / *beam_size as f64 + }) + .collect() + } else { + weights + .into_iter() + .zip(starts.iter()) + .map(|(_, (_, beam_size))| 1.0 / *beam_size as f64) + .collect() + } + } + } else { + let mut weights = weights + .iter() + .map(|v| v.to_continuous()) + .collect::>(); + + if self.use_cost_weight { + weights + .iter_mut() + .zip(starts.iter()) + .for_each(|(v, (_, beam_size))| *v /= *beam_size as f64); + } - if !search_non_positive && self.use_cost_weight { weights - .iter_mut() - .zip(starts.iter()) - .for_each(|(v, (_, beam_size))| *v /= *beam_size as f64); - } + }; let dist = WeightedIndex::new(weights).unwrap(); diff --git a/dypdl/Cargo.toml b/dypdl/Cargo.toml index aec9cfd..10f81da 100644 --- a/dypdl/Cargo.toml +++ b/dypdl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dypdl" -version = "0.7.1" +version = "0.7.2" edition = "2021" rust-version = "1.65" description = "Libarary for Dynamic Programming Description Language (DyPDL)."