diff --git a/.github/workflows/batsat.yml b/.github/workflows/batsat.yml new file mode 100644 index 00000000..cd5ef4df --- /dev/null +++ b/.github/workflows/batsat.yml @@ -0,0 +1,30 @@ +name: BatSat + +on: + push: + branches: [ "main", "next-major" ] + pull_request: + branches: [ "main", "next-major" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build-test: + name: Build and test + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "build-test" + - name: Cargo build + run: cargo build -p rustsat-batsat --verbose + - name: Cargo test + run: cargo test -p rustsat-batsat --verbose diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 82831b8e..9a954b6c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,7 +32,7 @@ jobs: echo "failed for main crate" return=false fi - for dir in tools cadical kissat minisat glucose ipasir capi pyapi; do + for dir in tools cadical kissat minisat glucose batsat ipasir capi pyapi; do cd ${dir} if ! cargo rdme --check; then echo "failed for ${dir}" diff --git a/.github/workflows/semver-checks.yml b/.github/workflows/semver-checks.yml index d756f6eb..3b073590 100644 --- a/.github/workflows/semver-checks.yml +++ b/.github/workflows/semver-checks.yml @@ -43,3 +43,7 @@ jobs: uses: obi1kenobi/cargo-semver-checks-action@v2 with: package: rustsat-ipasir + - name: BatSat + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + package: rustsat-batsat \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 0f250fa7..ff786a14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "solvertests", "capi", "pyapi", + "batsat", ] resolver = "2" diff --git a/batsat/.gitignore b/batsat/.gitignore new file mode 100644 index 00000000..2ebc5ea0 --- /dev/null +++ b/batsat/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock \ No newline at end of file diff --git a/batsat/CHANGELOG.md b/batsat/CHANGELOG.md new file mode 100644 index 00000000..c582dab2 --- /dev/null +++ b/batsat/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.1.0] - + +Initial Release + + diff --git a/batsat/Cargo.toml b/batsat/Cargo.toml new file mode 100644 index 00000000..d23d95bb --- /dev/null +++ b/batsat/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "rustsat-batsat" +version = "0.1.0" +edition = "2021" +authors = ["Noah Bruns "] +license = "MIT" +description = "Interface to the SAT solver BatSat for the RustSAT library. BatSat is fully implemented in Rust" +keywords = ["sat-solver", "rustsat", "batsat"] +repository = "https://github.com/chrjabs/rustsat" +readme = "README.md" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +batsat = "0.5.0" # when changing this version, do not forget to update signature +anyhow.workspace = true +thiserror.workspace = true +rustsat.workspace = true + +[dev-dependencies] +rustsat-solvertests = { path = "../solvertests" } diff --git a/batsat/README.md b/batsat/README.md new file mode 100644 index 00000000..218809a9 --- /dev/null +++ b/batsat/README.md @@ -0,0 +1,18 @@ +[![Check & Test](https://github.com/chrjabs/rustsat/actions/workflows/batsat.yml/badge.svg)](https://github.com/chrjabs/rustsat/actions/workflows/batsat.yml) +[![crates.io](https://img.shields.io/crates/v/rustsat-batsat)](https://crates.io/crates/rustsat-batsat) +[![docs.rs](https://img.shields.io/docsrs/rustsat-batsat)](https://docs.rs/rustsat-batsat) +[![License](https://img.shields.io/crates/l/rustsat-batsat)](../LICENSE) + + + +# rustsat-batsat - Interface to the BatSat SAT Solver for RustSAT + +Interface to the [BatSat](https://github.com/c-cube/batsat) incremental SAT-Solver to be used with the [RustSAT](https://github.com/chrjabs/rustsat) library. + +BatSat is fully implemented in Rust which has advantages in restricted compilation scenarios like WebAssembly. + +# BatSat Version + +The version of BatSat in this crate is Version 0.5.0. + + diff --git a/batsat/data b/batsat/data new file mode 120000 index 00000000..4909e06e --- /dev/null +++ b/batsat/data @@ -0,0 +1 @@ +../data \ No newline at end of file diff --git a/batsat/src/lib.rs b/batsat/src/lib.rs new file mode 100644 index 00000000..5937ffd9 --- /dev/null +++ b/batsat/src/lib.rs @@ -0,0 +1,125 @@ +//! # rustsat-batsat - Interface to the BatSat SAT Solver for RustSAT +//! +//! Interface to the [BatSat](https://github.com/c-cube/batsat) incremental SAT-Solver to be used with the [RustSAT](https://github.com/chrjabs/rustsat) library. +//! +//! BatSat is fully implemented in Rust which has advantages in restricted compilation scenarios like WebAssembly. +//! +//! # BatSat Version +//! +//! The version of BatSat in this crate is Version 0.5.0. + +#![warn(clippy::pedantic)] +#![warn(missing_docs)] + +use batsat::{intmap::AsIndex, lbool, BasicSolver, SolverInterface}; +use rustsat::{ + solvers::{Solve, SolveIncremental, SolverResult}, + types::{Clause, Lit, TernaryVal}, +}; +use thiserror::Error; + +/// API Error from the BatSat library (for example if the solver is in an UNSAT state) +#[derive(Error, Clone, Copy, PartialEq, Eq, Debug)] +#[error("BatSat returned an invalid value: {error}")] +pub struct InvalidApiReturn { + error: &'static str, +} + +/// RustSAT wrapper for the [`BasicSolver`] Solver from BatSat +#[derive(Default)] +pub struct BatsatBasicSolver(BasicSolver); + +impl Extend for BatsatBasicSolver { + fn extend>(&mut self, iter: T) { + iter.into_iter() + .for_each(|cl| self.add_clause(cl).expect("Error adding clause in extend")); + } +} + +impl<'a> Extend<&'a Clause> for BatsatBasicSolver { + fn extend>(&mut self, iter: T) { + iter.into_iter().for_each(|cl| { + self.add_clause_ref(cl) + .expect("Error adding clause in extend"); + }); + } +} + +impl Solve for BatsatBasicSolver { + fn signature(&self) -> &'static str { + "BatSat 0.5.0" + } + + fn solve(&mut self) -> anyhow::Result { + match self.0.solve_limited(&[]) { + x if x == lbool::TRUE => Ok(SolverResult::Sat), + x if x == lbool::FALSE => Ok(SolverResult::Unsat), + x if x == lbool::UNDEF => Err(InvalidApiReturn { + error: "BatSat Solver is in an UNSAT state", + } + .into()), + _ => unreachable!(), + } + } + + fn lit_val(&self, lit: Lit) -> anyhow::Result { + let l = batsat::Lit::new(batsat::Var::from_index(lit.vidx() + 1), lit.is_pos()); + + match self.0.value_lit(l) { + x if x == lbool::TRUE => Ok(TernaryVal::True), + x if x == lbool::FALSE => Ok(TernaryVal::False), + x if x == lbool::UNDEF => Ok(TernaryVal::DontCare), + _ => unreachable!(), + } + } + + fn add_clause(&mut self, clause: Clause) -> anyhow::Result<()> { + let mut c: Vec = clause + .iter() + .map(|l| batsat::Lit::new(self.0.var_of_int(l.vidx32() + 1), l.is_pos())) + .collect::>(); + + self.0.add_clause_reuse(&mut c); + + Ok(()) + } + + fn add_clause_ref(&mut self, clause: &Clause) -> anyhow::Result<()> { + let mut c: Vec = clause + .iter() + .map(|l| batsat::Lit::new(self.0.var_of_int(l.vidx32() + 1), l.is_pos())) + .collect::>(); + + self.0.add_clause_reuse(&mut c); + + Ok(()) + } +} + +impl SolveIncremental for BatsatBasicSolver { + fn solve_assumps(&mut self, assumps: &[Lit]) -> anyhow::Result { + let a = assumps + .iter() + .map(|l| batsat::Lit::new(self.0.var_of_int(l.vidx32() + 1), l.is_pos())) + .collect::>(); + + match self.0.solve_limited(&a) { + x if x == lbool::TRUE => Ok(SolverResult::Sat), + x if x == lbool::FALSE => Ok(SolverResult::Unsat), + x if x == lbool::UNDEF => Err(InvalidApiReturn { + error: "BatSat Solver is in an UNSAT state", + } + .into()), + _ => unreachable!(), + } + } + + fn core(&mut self) -> anyhow::Result> { + Ok(self + .0 + .unsat_core() + .iter() + .map(|l| Lit::new(l.var().idx() - 1, !l.sign())) + .collect::>()) + } +} diff --git a/batsat/tests/incremental.rs b/batsat/tests/incremental.rs new file mode 100644 index 00000000..f76eb878 --- /dev/null +++ b/batsat/tests/incremental.rs @@ -0,0 +1 @@ +rustsat_solvertests::incremental_tests!(rustsat_batsat::BatsatBasicSolver); diff --git a/batsat/tests/small.rs b/batsat/tests/small.rs new file mode 100644 index 00000000..a6f444c0 --- /dev/null +++ b/batsat/tests/small.rs @@ -0,0 +1,3 @@ +mod base { + rustsat_solvertests::base_tests!(rustsat_batsat::BatsatBasicSolver, false, true); +} diff --git a/clippy.toml b/clippy.toml index eff43ef7..a9773d0a 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,2 +1,2 @@ -doc-valid-idents = ["RustSAT", "CaDiCaL", "MaxSAT", ".."] +doc-valid-idents = ["RustSAT", "CaDiCaL", "MaxSAT", "BatSat", ".."] avoid-breaking-exported-api = false diff --git a/release-plz.toml b/release-plz.toml index fe2e31a4..28ca80fb 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -40,3 +40,8 @@ git_release_enable = false name = "rustsat-pyapi" release = false git_release_enable = false + +[[package]] +name = "rustsat-batsat" +release = false +git_release_enable = false