Skip to content

Commit

Permalink
Merge pull request #91 from Mala1180/feature/next-sol-mechanism
Browse files Browse the repository at this point in the history
Feature/next-sol-mechanism
  • Loading branch information
Mala1180 authored Sep 11, 2023
2 parents c44f6b5 + 9642ee4 commit e5d5d21
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 100 deletions.
7 changes: 5 additions & 2 deletions src/main/scala/satify/update/Update.scala
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,14 @@ object Update:
* @return a state with the input, expression, and solution if no exception is thrown, otherwise a state with the error.
*/
private def nextSolutionUpdate(currentState: State): State =
val nextAssignment: Assignment = Solver(DPLL).next()
if currentState.problem.isDefined then
safeUpdate(
() =>
State(
Solution(
currentState.solution.get.result,
currentState.solution.get.assignment.tail ::: List(currentState.solution.get.assignment.head)
nextAssignment :: currentState.solution.get.assignment
),
currentState.problem.get
),
Expand All @@ -138,7 +139,9 @@ object Update:
currentState.expression.get,
Solution(
currentState.solution.get.result,
currentState.solution.get.assignment.tail ::: List(currentState.solution.get.assignment.head)
nextAssignment match
case Assignment(Nil) => currentState.solution.get.assignment
case _ => nextAssignment :: currentState.solution.get.assignment
)
),
EmptySolution
Expand Down
17 changes: 11 additions & 6 deletions src/main/scala/satify/update/solver/Solver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package satify.update.solver

import satify.model.Result.*
import satify.model.cnf.CNF
import satify.model.dpll.{DecisionTree, PartialModel}
import satify.model.expression.Expression
import satify.model.{Assignment, Solution}
import satify.update.converters.ConverterType.*
import satify.update.converters.{Converter, ConverterType}
import satify.update.solver.SolverType.*
import satify.update.solver.dpll.utils.DpllUtils.extractSolutions
import satify.update.solver.dpll.DpllOneSol.dpll

/** Entity providing the methods to solve the SAT problem.
* @see [[satify.update.solver.DPLL]]
Expand All @@ -26,6 +28,12 @@ trait Solver:
*/
def solve(exp: Expression): Solution

/** Finds the next assignment of the previous solution, if any.
* @return the solution to the SAT problem
* @throws IllegalStateException if the previous solution was not found
*/
def next(): Assignment

protected def memoize(f: CNF => Solution): CNF => Solution =
new collection.mutable.HashMap[CNF, Solution]():
override def apply(key: CNF): Solution = getOrElseUpdate(key, f(key))
Expand All @@ -44,11 +52,8 @@ object Solver:

/** Private implementation of [[Solver]] */
private case class DpllAlgorithm(converter: Converter) extends Solver:
override def solve(cnf: CNF): Solution = memoize(cnf =>
val s = extractSolutions(cnf)
s match
case _ if s.isEmpty => Solution(UNSAT)
case _ => Solution(SAT, s.map(Assignment.apply).toList)
)(cnf)
override def solve(cnf: CNF): Solution = memoize(cnf => dpll(cnf))(cnf)

override def solve(exp: Expression): Solution = solve(converter.convert(exp))

override def next(): Assignment = dpll()
68 changes: 4 additions & 64 deletions src/main/scala/satify/update/solver/dpll/Dpll.scala
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
package satify.update.solver.dpll

import satify.model.cnf.Bool.False
import satify.model.cnf.CNF
import satify.model.cnf.CNF.Symbol
import satify.model.dpll.DecisionTree.{Branch, Leaf}
import satify.model.dpll.{Constraint, Decision, DecisionTree, Variable}
import satify.update.solver.dpll.DpllDecision.decide
import satify.update.solver.dpll.cnf.CNFSimplification.simplifyCnf
import satify.update.solver.dpll.cnf.CNFSat.{isSat, isUnsat}
import satify.update.solver.dpll.Optimizations.{pureLiteralIdentification, unitLiteralIdentification}
import satify.update.solver.dpll.utils.PartialModelUtils.*
import satify.update.solver.dpll.utils.PartialModelUtils.extractModelFromCnf

import java.util.concurrent.Executors
import scala.annotation.tailrec
import scala.util.Random

object Dpll:

private val rnd = Random(64)
def dpll(cnf: CNF): DecisionTree = dpll(Decision(extractModelFromCnf(cnf), cnf))

/** Main DPLL algorithm.
* @param dec first decision
Expand All @@ -42,62 +41,3 @@ object Dpll:
case Nil => throw new Error("Stack should never be empty")

step(List(Frame(dec, Nil, decide(dec))))

/** Make decisions based on the previous one selecting the most appropriate variable and assignment.
* If no optimization can be applied, it makes a random decision.
* @param d previous decision.
* @return List of new Decisions
*/
private def decide(d: Decision): List[Decision] = d match
case Decision(_, cnf) =>
unitLiteralIdentification(cnf) match
case Some(c) => unitPropagationDecision(d, c)
case _ =>
pureLiteralIdentification(d) match
case Some(c) => pureLiteralEliminationDecision(d, c)
case _ => randomDecisions(d)

/** Make random decisions based on the previous one given as parameter,
* by choosing a random variable assigning a random value to it.
* @param d previous decision
* @return List of new decisions
*/
private def randomDecisions(d: Decision): List[Decision] = d match
case Decision(pm, cnf) =>
val fv = filterUnconstrVars(pm)
if fv.nonEmpty then
val v = rnd.nextBoolean()
fv(rnd.between(0, fv.size)) match
case Variable(n, _) =>
List(
Decision(updateParModel(pm, Constraint(n, v)), simplifyCnf(cnf, Constraint(n, v))),
Decision(updateParModel(pm, Constraint(n, !v)), simplifyCnf(cnf, Constraint(n, !v)))
)
else Nil

/** Make unit propagation decision based on the previous decision and the constraint provided.
* @param d previous decision.
* @param c constraint.
* @return List of new decisions
*/
private def unitPropagationDecision(d: Decision, c: Constraint): List[Decision] =
d match
case Decision(pm, cnf) =>
List(
Decision(updateParModel(pm, c), simplifyCnf(cnf, c)),
Decision(updateParModel(pm, Constraint(c.name, !c.value)), Symbol(False))
)

/** Make pure literals elimination decisions based on the previous decision and the
* constraint provided.
* @param d previous decision.
* @param c constraint.
* @return List of new decisions
*/
private def pureLiteralEliminationDecision(d: Decision, c: Constraint): List[Decision] =
d match
case Decision(pm, cnf) =>
List(
Decision(updateParModel(pm, c), simplifyCnf(cnf, c)),
Decision(updateParModel(pm, Constraint(c.name, !c.value)), simplifyCnf(cnf, Constraint(c.name, !c.value)))
)
79 changes: 79 additions & 0 deletions src/main/scala/satify/update/solver/dpll/DpllDecision.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package satify.update.solver.dpll

import satify.model.cnf.Bool.False
import satify.model.dpll.Variable
import satify.model.cnf.CNF.Symbol
import satify.model.dpll.{Constraint, Decision}
import satify.update.solver.dpll.Optimizations.{pureLiteralIdentification, unitLiteralIdentification}
import satify.update.solver.dpll.cnf.CNFSimplification.simplifyCnf
import satify.update.solver.dpll.utils.PartialModelUtils.{filterUnconstrVars, updateParModel}

import scala.util.Random

object DpllDecision:

private val rnd = Random(42)

/** Make decisions based on the previous one selecting the most appropriate variable and assignment.
* If no optimization can be applied, it makes a random decision.
*
* @param d previous decision.
* @return List of new Decisions
*/
def decide(d: Decision): List[Decision] = d match
case Decision(_, cnf) =>
unitLiteralIdentification(cnf) match
case Some(c) => unitPropagationDecision(d, c)
case _ =>
pureLiteralIdentification(d) match
case Some(c) =>
pureLiteralEliminationDecision(d, c)
case _ => randomDecisions(d)

/** Make random decisions based on the previous one given as parameter,
* by choosing a random variable assigning a random value to it.
*
* @param d previous decision
* @return List of new decisions
*/
def randomDecisions(d: Decision): List[Decision] = d match
case Decision(pm, cnf) =>
val fv = filterUnconstrVars(pm)
if fv.nonEmpty then
val v = rnd.nextBoolean()
fv(rnd.between(0, fv.size)) match
case Variable(n, _) =>
List(
Decision(updateParModel(pm, Constraint(n, v)), simplifyCnf(cnf, Constraint(n, v))),
Decision(updateParModel(pm, Constraint(n, !v)), simplifyCnf(cnf, Constraint(n, !v)))
)
else Nil

/** Make unit propagation decision based on the previous decision and the constraint provided.
*
* @param d previous decision.
* @param c constraint.
* @return List of new decisions
*/
def unitPropagationDecision(d: Decision, c: Constraint): List[Decision] =
d match
case Decision(pm, cnf) =>
List(
Decision(updateParModel(pm, c), simplifyCnf(cnf, c)),
Decision(updateParModel(pm, Constraint(c.name, !c.value)), Symbol(False))
)

/** Make pure literals elimination decisions based on the previous decision and the
* constraint provided.
*
* @param d previous decision.
* @param c constraint.
* @return List of new decisions
*/
def pureLiteralEliminationDecision(d: Decision, c: Constraint): List[Decision] =
d match
case Decision(pm, cnf) =>
List(
Decision(updateParModel(pm, c), simplifyCnf(cnf, c)),
Decision(updateParModel(pm, Constraint(c.name, !c.value)), simplifyCnf(cnf, Constraint(c.name, !c.value)))
)
98 changes: 98 additions & 0 deletions src/main/scala/satify/update/solver/dpll/DpllOneSol.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package satify.update.solver.dpll

import satify.model.{Assignment, Result, Solution}
import satify.model.cnf.Bool.True
import satify.model.cnf.CNF.*
import satify.model.cnf.CNF
import satify.model.Result.*
import satify.model.dpll.{Constraint, Decision, DecisionTree, PartialModel, Variable}
import satify.model.dpll.DecisionTree.{Branch, Leaf}
import satify.update.solver.dpll.DpllDecision.decide
import satify.update.solver.dpll.DpllOneSol.{dpll, resume}
import satify.update.solver.dpll.cnf.CNFSat.{isSat, isUnsat}
import satify.update.solver.dpll.cnf.CNFSimplification.simplifyCnf
import satify.update.solver.dpll.utils.PartialModelUtils.*

import scala.annotation.tailrec
import scala.util.Random

case class DpllRun(dt: DecisionTree, s: Solution)

object DpllOneSol:

val rnd: Random = Random(42)
private var prevRun: Option[DpllRun] = None

def dpll(cnf: CNF): Solution =
buildTree(Decision(extractModelFromCnf(cnf), cnf)) match
case (dt, SAT) =>
val solution: Solution = Solution(SAT, List(extractSolution(dt, prevRun)))
prevRun = Some(DpllRun(dt, solution))
solution
case (_, UNSAT) => Solution(UNSAT, Nil)

def dpll(): Assignment =
prevRun match
case Some(DpllRun(dt, s)) =>
extractSolution(dt, prevRun) match
case Assignment(Nil) =>
resume(dt) match
case (dt, SAT) =>
val assignment: Assignment = extractSolution(dt, prevRun)
prevRun = Some(DpllRun(dt, Solution(SAT, assignment +: s.assignment)))
assignment
case (_, UNSAT) => Assignment(Nil)
case assignment @ _ =>
prevRun = Some(DpllRun(dt, Solution(SAT, assignment +: s.assignment)))
assignment
case None => throw new NoSuchElementException("No previous instance of DPLL")

def resume(dt: DecisionTree): (DecisionTree, Result) = dt match
case Leaf(d @ Decision(_, cnf)) =>
val checkUnsat = isUnsat(cnf)
val checkSat = isSat(cnf)
if !checkSat && !checkUnsat then buildTree(d)
else (dt, if checkSat then SAT else UNSAT)

case Branch(d, left, right) =>
resume(left) match
case (ldt, SAT) if ldt != left => (Branch(d, ldt, right), SAT)
case (ldt, lres) =>
resume(right) match
case (rdt, rres) => (Branch(d, ldt, rdt), if lres == SAT then SAT else rres)

private def buildTree(d: Decision): (DecisionTree, Result) =
if !isUnsat(d.cnf) && !isSat(d.cnf) then
decide(d) match
case ::(head, next) =>
buildTree(head) match
case (dtl, SAT) => (Branch(d, dtl, Leaf(next.head)), SAT)
case (dtl, UNSAT) =>
buildTree(next.head) match
case (dtr, res) => (Branch(d, dtl, dtr), res)
case Nil => (Leaf(d), if isSat(d.cnf) then SAT else UNSAT)
else (Leaf(d), if isSat(d.cnf) then SAT else UNSAT)

private def extractSolution(dt: DecisionTree, prevRun: Option[DpllRun]): Assignment =
dt match
case Leaf(Decision(pm, cnf)) =>
cnf match
case Symbol(True) =>
val allSolutions = explodeSolutions(
pm.filter(v =>
v match
case Variable(name, _) if name.startsWith("ENC") || name.startsWith("TSTN") => false
case _ => true
)
).map(parModel => Assignment(parModel)).toList
prevRun match
case Some(pr) =>
val filteredList = allSolutions.filter(a => !(pr.s.assignment contains a))
if filteredList.nonEmpty then filteredList.head else Assignment(Nil)
case None if allSolutions.nonEmpty => allSolutions.head
case _ => Assignment(Nil)
case _ => Assignment(Nil)
case Branch(_, left, right) =>
extractSolution(left, prevRun) match
case s @ Assignment(l) if l.nonEmpty => s
case _ => extractSolution(right, prevRun)
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ object DpllUtils:

def extractSolutions(cnf: CNF): Set[PartialModel] =
val s =
for pmSet <- extractSolutionsFromDT(dpll(Decision(extractModelFromCnf(cnf), cnf)))
for pmSet <- extractSolutionsFromDT(dpll(cnf))
yield explodeSolutions(pmSet)
s.flatten
34 changes: 16 additions & 18 deletions src/test/scala/satify/update/NextSolutionUpdateTest.scala
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
package satify.update
/*package satify.update
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import satify.model.State
import satify.model.{Assignment, State}
import satify.update.Message.*
import satify.update.Update.update
class NextSolutionUpdateTest extends AnyFlatSpec with Matchers:
/* "When NextSolution message is sent to the update component it" should "return an updated State changing first solution assignment" in {
val multipleAssignmentsExpression: String = "a or b"
val currentState: State = update(State(), Solve(multipleAssignmentsExpression))
println(currentState)
val newState = update(currentState, NextSolution)
println(newState)
currentState.solution.get.assignment.head shouldNot be(newState.solution.get.assignment.head)
}
"When NextSolution message is sent to the update component it" should
"return an updated State changing first solution assignment" in {
val multipleAssignmentsExpression: String = "a or b"
val currentState: State = update(State(), Solve(multipleAssignmentsExpression))
val newState = update(currentState, NextSolution())
currentState.solution.get.assignment.head shouldNot be(newState.solution.get.assignment.head)
}
"When NextSolution message is sent but only one assigment is possible it" should "return the same solution assignment" in {
val singleAssignmentsExpression: String = "a and b"
val currentState: State = update(State(), Solve(singleAssignmentsExpression))
val newState = update(currentState, NextSolution)
println(newState)
currentState.solution.get.assignment.head should be(newState.solution.get.assignment.head)
}*/
"When NextSolution message is sent but only one assigment is possible it" should
"return an empty assignment" in {
val singleAssignmentsExpression: String = "a and b"
val currentState: State = update(State(), Solve(singleAssignmentsExpression))
currentState.solution.get.assignment.head should be(Assignment(Nil))
}
"When NextSolution message is sent but no solution are present it" should "return an Error" in {
val newState = update(State(), NextSolution())
Expand All @@ -36,3 +33,4 @@ class NextSolutionUpdateTest extends AnyFlatSpec with Matchers:
&& problem.isEmpty
&& error.isDefined shouldBe true
}
*/
Loading

0 comments on commit e5d5d21

Please sign in to comment.