1#![allow(dead_code, clippy::cast_precision_loss)]
2
3use crate::sat::assignment::{AssignmentImpls, AssignmentType};
4use crate::sat::cdcl::Cdcl;
5use crate::sat::clause_management::{
6 ClauseManagement, ClauseManagementImpls, LbdClauseManagement, NoClauseManagement,
7};
8use crate::sat::clause_storage::{LiteralStorage, LiteralStorageImpls, LiteralStorageType};
9use crate::sat::cnf::Cnf;
10use crate::sat::dimacs::parse_file;
11use crate::sat::dpll::Dpll;
12use crate::sat::literal::{Literal, LiteralImpls, LiteralType};
13use crate::sat::propagation::{PropagatorImpls, PropagatorType};
14use crate::sat::restarter::{RestarterImpls, RestarterType};
15use crate::sat::solver::{
16 DynamicConfig, SolutionStats, Solutions, Solver, SolverImpls, SolverType,
17};
18use crate::sat::variable_selection::{VariableSelectionImpls, VariableSelectionType};
19use crate::{nonogram, sudoku};
20use clap::{Args, Parser, Subcommand};
21use std::path::PathBuf;
22use std::time::Duration;
23use tikv_jemalloc_ctl::{epoch, stats};
24
25#[derive(Parser, Debug)]
29#[command(name = "sat_solver", version, about = "A configurable SAT solver")]
30pub(crate) struct Cli {
31 #[arg(global = true)]
35 pub path: Option<PathBuf>,
36
37 #[clap(subcommand)]
39 pub command: Option<Commands>,
40
41 #[command(flatten)]
43 pub common: CommonOptions,
44}
45
46#[derive(Subcommand, Debug)]
48pub(crate) enum Commands {
49 File {
51 #[arg(long)]
53 path: PathBuf,
54
55 #[command(flatten)]
57 common: CommonOptions,
58 },
59
60 Text {
62 #[arg(short, long)]
65 input: String,
66
67 #[command(flatten)]
69 common: CommonOptions,
70 },
71
72 Sudoku {
75 #[arg(long)]
77 path: PathBuf,
78
79 #[arg(short, long, default_value_t = false)]
81 export_dimacs: bool,
82
83 #[command(flatten)]
85 common: CommonOptions,
86 },
87
88 Nonogram {
91 #[arg(long)]
93 path: PathBuf,
94
95 #[command(flatten)]
97 common: CommonOptions,
98 },
99
100 Completions {
102 #[arg(value_enum)]
104 shell: clap_complete::Shell,
105 },
106}
107
108#[derive(Args, Debug, Default, Clone)]
110#[allow(clippy::struct_excessive_bools)]
111pub(crate) struct CommonOptions {
112 #[arg(short, long, default_value_t = false)]
114 pub(crate) debug: bool,
115
116 #[arg(short, long, default_value_t = true)]
118 pub(crate) verify: bool,
119
120 #[arg(short, long, default_value_t = true)]
122 pub(crate) stats: bool,
123
124 #[arg(short, long, default_value_t = false)]
126 pub(crate) print_solution: bool,
127
128 #[arg(long, default_value_t = SolverType::Cdcl)]
131 solver: SolverType,
132
133 #[arg(long, default_value_t = false)]
135 no_clause_management: bool,
136
137 #[arg(long, default_value_t = RestarterType::Luby)]
138 restart_strategy: RestarterType,
139
140 #[arg(long, default_value_t = VariableSelectionType::Vsids)]
141 variable_selection: VariableSelectionType,
142
143 #[arg(long, default_value_t = LiteralStorageType::SmallVec)]
144 literal_storage: LiteralStorageType,
145
146 #[arg(long, default_value_t = AssignmentType::Vec)]
147 assignment: AssignmentType,
148
149 #[arg(long, default_value_t = PropagatorType::WatchedLiterals)]
150 propagator: PropagatorType,
151
152 #[arg(long, default_value_t = LiteralType::Double)]
153 literals: LiteralType,
154}
155
156pub(crate) fn get_solver_parts<L: Literal, S: LiteralStorage<L>>(
158 common: &CommonOptions,
159 cnf: &Cnf<L, S>,
160) -> (
161 AssignmentImpls,
162 ClauseManagementImpls<10>,
163 PropagatorImpls<LiteralImpls, LiteralStorageImpls<LiteralImpls, 12>, AssignmentImpls>,
164 VariableSelectionImpls,
165 RestarterImpls<3>,
166) {
167 let manager = if common.no_clause_management {
168 ClauseManagementImpls::NoClauseManagement(NoClauseManagement::new(&cnf.clauses))
169 } else {
170 ClauseManagementImpls::LbdActivityClauseManagement(LbdClauseManagement::new(&cnf.clauses))
171 };
172 let cnf1: Cnf<LiteralImpls, LiteralStorageImpls<LiteralImpls, 12>> = cnf.convert();
173 let propagator = common.propagator.to_impl(&cnf1);
174
175 (
176 common.assignment.to_impl(cnf.num_vars),
177 manager,
178 propagator,
179 common
180 .variable_selection
181 .to_impl(cnf.num_vars, &cnf.lits, &cnf.clauses),
182 common.restart_strategy.to_impl(),
183 )
184}
185
186pub(crate) fn get_solver<L: Literal, S: LiteralStorage<L>>(
187 common: &CommonOptions,
188 cnf: &Cnf<L, S>,
189) -> SolverImpls<DynamicConfig> {
190 let (assignment, manager, propagator, selector, restarter) = get_solver_parts(common, cnf);
191
192 match common.solver {
193 SolverType::Cdcl => SolverImpls::Cdcl(Box::from(Cdcl::<DynamicConfig>::from_parts(
194 cnf.clone(),
195 assignment,
196 manager,
197 propagator,
198 restarter,
199 selector,
200 ))),
201 SolverType::Dpll => SolverImpls::Dpll(Box::from(Dpll::<DynamicConfig>::from_parts(
202 cnf.clone(),
203 assignment,
204 manager,
205 propagator,
206 restarter,
207 selector,
208 ))),
209 }
210}
211
212pub(crate) fn solve_dir(path: &PathBuf, common: &CommonOptions) -> Result<(), String> {
223 if !path.is_dir() {
224 eprintln!("Provided path is not a directory: {}", path.display());
225 std::process::exit(1);
226 }
227
228 for entry in walkdir::WalkDir::new(path)
229 .into_iter()
230 .filter_map(Result::ok)
231 {
232 let file_path = entry.path().to_path_buf();
233 if file_path.extension().is_some_and(|ext| ext == "sudoku") {
234 solve_sudoku(&file_path, false, common)?;
235 continue;
236 }
237
238 if file_path.extension().is_none_or(|ext| ext != "cnf") {
239 eprintln!("Skipping non-CNF file: {}", file_path.display());
240 continue;
241 }
242
243 if !file_path.is_file() {
244 eprintln!("Skipping non-file entry: {}", file_path.display());
245 continue;
246 }
247
248 if !file_path.exists() {
249 eprintln!("File does not exist: {}", file_path.display());
250 continue;
251 }
252
253 let time = std::time::Instant::now();
254 let result = parse_file(&file_path);
255 if let Err(e) = result {
256 return Err(e.to_string());
257 }
258 let cnf = result.unwrap();
259 let elapsed = time.elapsed();
260
261 solve_and_report(&cnf, common, Some(&file_path), elapsed);
262 }
263
264 Ok(())
265}
266
267pub(crate) fn verify_solution(cnf: &Cnf, sol: Option<&Solutions>) {
276 if let Some(sol_values) = sol {
277 let ok = cnf.verify(sol_values);
278 println!("Verified: {ok:?}");
279 assert!(ok, "Solution failed verification!");
280 } else {
281 println!("UNSAT");
282 }
283}
284
285pub(crate) fn solve(
302 cnf: &Cnf,
303 debug: bool,
304 label: Option<&PathBuf>,
305 common_options: &CommonOptions,
306) -> (Option<Solutions>, Duration, SolutionStats) {
307 if let Some(name) = label {
308 println!("Solving: {}", name.display());
309 }
310
311 if debug {
312 println!("CNF: {cnf}");
313 println!("Variables: {}", cnf.num_vars);
314 println!("Clauses: {}", cnf.clauses.len());
315 println!("Literals: {}", cnf.lits.len());
316 }
317
318 solve_impl(cnf, common_options)
319}
320
321pub(crate) fn solve_impl(
330 cnf: &Cnf,
331 common_options: &CommonOptions,
332) -> (Option<Solutions>, Duration, SolutionStats) {
333 epoch::advance().unwrap();
334
335 let time = std::time::Instant::now();
336
337 let mut solver = get_solver(common_options, cnf);
338 let sol = solver.solve();
339
340 let elapsed = time.elapsed();
341
342 if common_options.debug {
343 println!("Solution: {sol:?}");
344 println!("Time: {elapsed:?}");
345 }
346
347 (sol, elapsed, solver.stats())
348}
349
350pub(crate) fn solve_and_report(
360 cnf: &Cnf,
361 common: &CommonOptions,
362 label: Option<&PathBuf>,
363 parse_time: Duration,
364) {
365 epoch::advance().unwrap();
366
367 let (sol, elapsed, solver_stats) = solve(cnf, common.debug, label, common);
368
369 epoch::advance().unwrap();
370
371 let allocated_bytes = stats::allocated::mib().unwrap().read().unwrap();
372 let resident_bytes = stats::resident::mib().unwrap().read().unwrap();
373
374 let allocated_mib = allocated_bytes as f64 / (1024.0 * 1024.0);
375 let resident_mib = resident_bytes as f64 / (1024.0 * 1024.0);
376
377 if common.verify {
378 verify_solution(cnf, Option::from(&sol));
379 }
380
381 if common.stats {
382 print_stats(
383 parse_time,
384 elapsed,
385 cnf,
386 &solver_stats,
387 allocated_mib,
388 resident_mib,
389 common.print_solution,
390 Option::from(&sol),
391 );
392 }
393}
394
395pub(crate) fn stat_line(label: &str, value: impl std::fmt::Display) {
401 println!("| {label:<28} {value:>18} |");
402}
403
404pub(crate) fn stat_line_with_rate(label: &str, value: usize, elapsed: f64) {
411 let rate = if elapsed > 0.0 {
412 value as f64 / elapsed
413 } else {
414 0.0
415 };
416 println!("| {label:<20} {value:>12} ({rate:>9.0}/sec) |");
417}
418
419#[allow(clippy::too_many_arguments)]
431pub(crate) fn print_stats(
432 parse_time: Duration,
433 elapsed: Duration,
434 cnf: &Cnf,
435 s: &SolutionStats,
436 allocated: f64,
437 resident: f64,
438 print_solution: bool,
439 solutions: Option<&Solutions>,
440) {
441 let elapsed_secs = elapsed.as_secs_f64();
442
443 println!("\n=======================[ Problem Statistics ]=========================");
444 stat_line("Parse time (s)", format!("{:.3}", parse_time.as_secs_f64()));
445 stat_line(
446 "Variables",
447 if cnf.num_vars > 0 {
448 cnf.num_vars - 1
449 } else {
450 0
451 },
452 );
453 stat_line("Clauses (original)", cnf.non_learnt_idx);
454 stat_line("Literals (original)", cnf.lits.len());
455
456 println!("========================[ Search Statistics ]========================");
457 stat_line("Learnt clauses", s.learnt_clauses);
458 stat_line("Total clauses (incl. learnt)", cnf.clauses.len());
459 stat_line_with_rate("Conflicts", s.conflicts, elapsed_secs);
460 stat_line_with_rate("Decisions", s.decisions, elapsed_secs);
461 stat_line_with_rate("Propagations", s.propagations, elapsed_secs);
462 stat_line_with_rate("Restarts", s.restarts, elapsed_secs);
463 stat_line("Memory usage (MiB)", format!("{allocated:.2}"));
464 stat_line("Resident memory (MiB)", format!("{resident:.2}"));
465 stat_line("CPU time (s)", format!("{elapsed_secs:.3}"));
466 println!("=====================================================================");
467
468 if let Some(solutions_values) = solutions {
469 if print_solution {
470 println!("Solutions: {solutions_values}");
471 }
472 }
473
474 if solutions.is_some() {
475 println!("\nSATISFIABLE");
476 } else {
477 println!("\nUNSATISFIABLE");
478 }
479}
480
481pub(crate) fn solve_sudoku(
487 path: &PathBuf,
488 export_dimacs: bool,
489 common: &CommonOptions,
490) -> Result<(), String> {
491 if !path.exists() {
492 return Err(format!("Sudoku file does not exist: {}", path.display()));
493 }
494
495 if !path.is_file() {
496 return Err(format!("Provided path is not a file: {}", path.display()));
497 }
498
499 let time = std::time::Instant::now();
500 let sudoku = sudoku::solver::parse_sudoku_file(path);
501
502 match sudoku {
503 Ok(sudoku) => {
504 println!("Parsed Sudoku:\n{sudoku}");
505
506 let cnf = sudoku.to_cnf();
507
508 if export_dimacs {
509 let dimacs = cnf.to_string();
510 println!("DIMACS:\n{dimacs}");
511
512 let path_name = path.display().to_string();
513 let dimacs_path = format!("{path_name}.cnf");
514 if let Err(e) = std::fs::write(&dimacs_path, dimacs) {
515 return Err(format!("Unable to write {dimacs_path}: {e}"));
516 }
517 println!("DIMACS written to: {dimacs_path}");
518 }
519
520 let parse_time = time.elapsed();
521 let (sol, elapsed, solver_stats) = solve(&cnf, common.debug, Some(path), common);
522
523 epoch::advance().unwrap();
524
525 let allocated_bytes = stats::allocated::mib().unwrap().read().unwrap();
526 let resident_bytes = stats::resident::mib().unwrap().read().unwrap();
527 let allocated_mib = allocated_bytes as f64 / (1024.0 * 1024.0);
528 let resident_mib = resident_bytes as f64 / (1024.0 * 1024.0);
529
530 if common.verify {
531 verify_solution(&cnf, Option::from(&sol));
532 }
533
534 if common.stats {
535 print_stats(
536 parse_time,
537 elapsed,
538 &cnf,
539 &solver_stats,
540 allocated_mib,
541 resident_mib,
542 common.print_solution,
543 Option::from(&sol),
544 );
545 }
546
547 if let Some(sol_values) = sol {
548 let solution_grid = sudoku.decode(&sol_values);
549 println!("Solution: {solution_grid}");
550 } else {
551 println!("No solution found");
552 }
553 }
554 Err(e) => {
555 return Err(format!("Error parsing Sudoku file: {e}"));
556 }
557 }
558 Ok(())
559}
560
561pub(crate) fn solve_nonogram(path: &PathBuf, common: &CommonOptions) -> Result<(), String> {
567 if !path.exists() {
568 return Err(format!("Nonogram file does not exist: {}", path.display()));
569 }
570
571 if !path.is_file() {
572 return Err(format!("Provided path is not a file: {}", path.display()));
573 }
574
575 let time = std::time::Instant::now();
576 let nonogram = nonogram::solver::parse_nonogram_file(path);
577 match nonogram {
578 Ok(nonogram) => {
579 println!("Parsed Nonogram:\n{nonogram}");
580
581 let cnf = nonogram.to_cnf();
582
583 let parse_time = time.elapsed();
584 let (sol, elapsed, solver_stats) = solve(&cnf, common.debug, Some(path), common);
585
586 epoch::advance().unwrap();
587 let allocated_bytes = stats::allocated::mib().unwrap().read().unwrap();
588 let resident_bytes = stats::resident::mib().unwrap().read().unwrap();
589 let allocated_mib = allocated_bytes as f64 / (1024.0 * 1024.0);
590 let resident_mib = resident_bytes as f64 / (1024.0 * 1024.0);
591
592 if common.verify {
593 verify_solution(&cnf, Option::from(&sol));
594 }
595
596 if common.stats {
597 print_stats(
598 parse_time,
599 elapsed,
600 &cnf,
601 &solver_stats,
602 allocated_mib,
603 resident_mib,
604 common.print_solution,
605 Option::from(&sol),
606 );
607 }
608
609 if let Some(sol_values) = sol {
610 let solution_grid = nonogram.decode(&sol_values);
611 for row in solution_grid {
612 println!("{row:?}");
613 }
614 } else {
615 println!("No solution found");
616 }
617 }
618 Err(e) => {
619 return Err(format!("Error parsing nonogram file: {e}"));
620 }
621 }
622 Ok(())
623}