Verified Commit e4920ba8 authored by Marcin Moskal's avatar Marcin Moskal
Browse files

Multivariable unification

parent dfeaa053
package pl.edu.agh.kis.eis
class KnowledgeBase[A](facts: Seq[MonoFact[A]]) {
def querySingle(f: (Variable[A] => Seq[Placeholder[A]])): Option[A] = {
val variable = Variable[A]()
val placeholders: Seq[Placeholder[A]] = f(variable)
class KnowledgeBase[A](val facts: Seq[MonoFact[A]]) {
def queryFirst(f: (Variable[A] => Seq[Placeholder[A]])): Option[A] = {
val variable = Variable[A](name = Some("x"))
queryDeterministic(f(variable)).headOption.flatMap(_.headOption).flatMap(_.binding)
}
def queryDeterministic(placeholder: Placeholder[A], knownBindings: Seq[Set[Variable[A]]] = Seq.empty):
Seq[Set[Variable[A]]] = {
val variables = placeholder.variables
val candidates = withArity(placeholder.arity).withMatchingConstants(placeholder.constants.toMap)
placeholders.find(!_.isBound).flatMap { p =>
facts.takeWhile { fact =>
p.unify(fact)
!facts.containsSlice(placeholders.map(_.buildFact()))
candidates.facts.map { fact =>
val unified = placeholder.unify(fact)
if(unified.nonEmpty) {
variables.map(x => Variable(x.binding, x.name)) // clones
} else {
Set.empty[Variable[A]]
}
variable.binding
}
}.filter(_.nonEmpty)
}
def queryDeterministic(placeholders: Seq[Placeholder[A]]): Seq[Set[Variable[A]]] =
placeholders.foldLeft(Seq.empty[Set[Variable[A]]]) { case (bindings, placeholder) =>
queryDeterministic(placeholder, bindings).flatMap(Variable.augmentBindings(bindings, _))
}
def queryNondeterministic(f: (Variable[A] => Seq[Placeholder[A]])): Set[A] = ???
def withArity(arity: Int): KnowledgeBase[A] =
new KnowledgeBase[A](facts.filter(_.arity == arity))
def withMatchingConstants(mappings: Map[Int, A]): KnowledgeBase[A] =
new KnowledgeBase(facts.filter(_.matchesConstants(mappings)))
}
package pl.edu.agh.kis.eis
case class MonoFact[A](values: A*)
import scala.util.{Success, Try}
case class MonoFact[A](values: A*) {
def arity: Int = values.size
def matchesConstants(mappings: Map[Int, A]): Boolean = mappings.forall { case (index, constant) =>
Try(values.apply(index)) == Success(constant) // FIXME Seq is inefficient repr (multiple scans)
}
}
......@@ -7,12 +7,12 @@ case class Placeholder[A](s: VarOrAtom[A]*) {
})
def buildFact(): MonoFact[A] = MonoFact(s.map {
case Variable(binding) => binding.get
case v: Variable[A] => v.binding.get
case Atom(a) => a
}: _*)
def unify(fact: MonoFact[A]): Seq[A] = {
val placeholderVars = s.zipWithIndex.collect { case (v@Variable(_), index) => (v, index) }.toSet
val placeholderVars = s.zipWithIndex.collect { case (v: Variable[A], index: Int) => (v, index) }.toSet
placeholderVars.foreach { case (variable, index) => variable.bind(fact.values(index)) }
if (buildFact() == fact) {
......@@ -21,14 +21,13 @@ case class Placeholder[A](s: VarOrAtom[A]*) {
Seq.empty
}
}
}
sealed trait VarOrAtom[A]
case class Variable[A](var binding: Option[A] = None) extends VarOrAtom[A] {
def isBound: Boolean = binding.isDefined
def bind(value: A): A = {
binding = Some(value)
value
def isBindingCompatible(binding: Set[Variable[A]]): Boolean = {
variables.filter(v => v.isBound && binding.exists(_.name == v.name)).forall(binding.contains)
}
lazy val arity: Int = s.size // FIXME common with MonoFact
lazy val constants: Seq[(Int, A)] = s.zipWithIndex.collect { case (Atom(const), index) => (index, const) }
lazy val variables: Set[Variable[A]] = s.collect { case v: Variable[A] => v }.toSet
}
case class Atom[A](a: A) extends VarOrAtom[A]
package pl.edu.agh.kis.eis
sealed trait VarOrAtom[A]
case class Variable[A](var binding: Option[A] = None, name: Option[String] = None) extends VarOrAtom[A] {
def isBound: Boolean = binding.isDefined
def bind(value: A): A = {
binding = Some(value)
value
}
}
object Variable {
def apply[A](value: A): Variable[A] = Variable(Some(value))
def apply[A](name: String): Variable[A] = Variable(name = Some(name))
def apply[A](name: String, value: A): Variable[A] = Variable(Some(value), Some(name))
def augmentBindings[A](knownBindings: Seq[Set[Variable[A]]], candidate: Set[Variable[A]]): Seq[Set[Variable[A]]] = {
if (knownBindings.isEmpty) {
return Seq(candidate)
}
val compatible = knownBindings.filter { knownBinding =>
val shared = knownBinding.filter(x => candidate.exists(_.name == x.name))
shared.forall(candidate.contains)
}
compatible.map(_ ++ candidate)
}
}
case class Atom[A](a: A) extends VarOrAtom[A]
\ No newline at end of file
......@@ -3,31 +3,92 @@ package pl.edu.agh.kis.eis
import org.scalatest.{FlatSpec, Matchers}
class KnowledgeBaseSpec extends FlatSpec with Matchers {
"KnowledgeBase" should "contain facts" in {
"First-match variable binding" should "be returned in" in {
val facts = Seq(
MonoFact(1),
MonoFact(2),
MonoFact(1, 2)
MonoFact(1),
MonoFact(1, 2),
MonoFact(2, 1)
)
val kb = new KnowledgeBase(facts)
kb.querySingle { x => Seq(Placeholder(x), Placeholder(Atom(1), x)) } should equal(Some(2))
kb.queryFirst { x => Seq(Placeholder(x), Placeholder(Atom(1), x)) } should equal(Some(2))
kb.queryFirst { x => Seq(Placeholder(x)) } should equal(Some(2))
kb.queryFirst { x => Seq(Placeholder(x, Atom(2))) } should equal(Some(1))
kb.queryFirst { x => Seq(Placeholder(Atom(1), x)) } should equal(Some(2))
}
it should "unify variable with unary fact" in {
val kb = new KnowledgeBase(Seq(MonoFact(1)))
kb.querySingle { x => Seq(Placeholder(x)) } should equal(Some(1))
"First-match variable binding on empty knowledge base" should "not be returned" in {
val kb = new KnowledgeBase(Seq.empty[MonoFact[Int]])
kb.queryFirst { x => Seq(Placeholder(x)) } should equal(None)
}
it should "unify one variable in binary fact" in {
val kb = new KnowledgeBase(Seq(MonoFact(1, 2)))
kb.querySingle { x => Seq(Placeholder(x, Atom(2))) } should equal(Some(1))
kb.querySingle { x => Seq(Placeholder(Atom(1), x)) } should equal(Some(2))
"Non-deterministic query" should "return valid variable binding" ignore {
val kb = new KnowledgeBase(Seq(
MonoFact(1, 1),
MonoFact(3, 3)
))
kb.queryNondeterministic { x: Variable[Int] => Seq(Placeholder(x, x)) } should contain allOf (1, 3)
}
it should "not return value for empty knowledge base" in {
val kb = new KnowledgeBase(Seq.empty[MonoFact[Int]])
kb.querySingle { x => Seq(Placeholder(x)) } should equal(None)
"Facts with specified arity" should "be returned" in {
val knowledgeBase: KnowledgeBase[Int] = new KnowledgeBase(Seq(
MonoFact(0),
MonoFact(1, 20),
MonoFact(1),
MonoFact(20, 1),
MonoFact(0, 0),
MonoFact(1, 0)
))
all (knowledgeBase.withArity(1).facts.map(_.arity)) shouldEqual 1
all (knowledgeBase.withArity(2).facts.map(_.arity)) shouldEqual 2
}
"Single multi-variable deterministic query" should "return valid variable bindings" in {
val x: Variable[Int] = Variable()
val y: Variable[Int] = Variable()
val kb: KnowledgeBase[Int] = new KnowledgeBase(Seq(
MonoFact(2),
MonoFact(1, 1),
MonoFact(2, 2),
MonoFact(1),
MonoFact(2, 1),
MonoFact(1, 2)
))
kb.queryDeterministic(Placeholder(x, x)) should equal(List(
Set(Variable(1)), Set(Variable(2))
))
kb.queryDeterministic(Placeholder(x, y)) should equal(List(
Set(Variable(1), Variable(1)),
Set(Variable(2), Variable(2)),
Set(Variable(2), Variable(1)),
Set(Variable(1), Variable(2))
))
kb.queryDeterministic(Placeholder(x)) should equal(List(
Set(Variable(2)),
Set(Variable(1))
))
}
"Compound, multi-variable deterministic query" should "return valid variable bindings" in {
val x = Variable[Int](name = Some("x"))
val y = Variable[Int](name = Some("y"))
val z = Variable[Int](name = Some("z"))
val kb = new KnowledgeBase[Int](Seq(
MonoFact(1, 2),
MonoFact(1, 3),
MonoFact(2, 1),
MonoFact(1)
))
kb.queryDeterministic(Seq(Placeholder(x), Placeholder(y, z))) should equal(Seq(
Set(Variable("x", 1), Variable("y", 1), Variable("z", 2)),
Set(Variable("x", 1), Variable("y", 1), Variable("z", 3)),
Set(Variable("x", 1), Variable("y", 2), Variable("z", 1))
))
}
}
package pl.edu.agh.kis.eis
import org.scalatest.{FlatSpec, Matchers}
class MonoFactSpec extends FlatSpec with Matchers {
"Constant variable match" should "success" in {
val fact = MonoFact(1, 2)
fact matchesConstants Map(0 -> 1) should be(true)
fact matchesConstants Map(1 -> 2) should be(true)
fact matchesConstants Map(0 -> 1, 1 -> 2) should be(true)
}
"Constant variable match" should "fail" in {
val fact = MonoFact(5, 7)
fact matchesConstants Map(2 -> 10) should be(false) // index off-by-one
// reversed values
fact matchesConstants Map(0 -> 7) should be(false)
fact matchesConstants Map(1 -> 5) should be(false)
}
}
......@@ -6,8 +6,8 @@ class PlaceholderSpec extends FlatSpec with Matchers {
def genVariables[A](n: Int): Seq[VarOrAtom[A]] = Seq.tabulate(n)(_ => Variable())
"Placeholder" should "bind single variable" in {
val variables = Seq(Variable[Int]())
val placeholder = Placeholder(variables.head, variables.head)
val x = Variable[Int]("x")//Seq(Variable[Int]())
val placeholder = Placeholder(x, x)
placeholder.unify(MonoFact(1, 1)) should contain only 1
}
......@@ -33,4 +33,33 @@ class PlaceholderSpec extends FlatSpec with Matchers {
placeholder.unify(MonoFact(1, 20, 20, 2, 18)) should contain inOrderOnly(20, 18)
placeholder.unify(MonoFact(1, 20, 30, 2, 18)) should be(empty)
}
"Placeholder constants" should "be returned" in {
Placeholder().constants should be(empty)
Placeholder(Variable(), Atom(1), Atom(2), Variable(), Atom(20)).constants should contain inOrderOnly(
(1, 1), (2, 2), (4, 20))
Placeholder(Atom(18), Variable(), Variable(), Variable(), Atom(1)).constants should contain inOrderOnly(
(0, 18), (4, 1)
)
}
"Placeholder variables" should "be returned" in {
Placeholder(Atom(1), Atom(1), Variable("x"), Variable("y"), Atom(2)).variables should contain only(
Variable("x"),
Variable("y")
)
}
"Provided variables" should "be compatible with Placeholder" in {
Placeholder(Variable("x", 5)).isBindingCompatible(Set(Variable("x", 5))) should equal(true)
}
"Provided variables" should "not be compatible with Placeholder" in {
Placeholder(Variable("x", 5)).isBindingCompatible(Set(Variable("x", 10))) should equal(false)
}
"Unrelated variable" should "be compatible with any Placeholder" in {
Placeholder(Variable("y", 20)).isBindingCompatible(Set(Variable("x", 20))) should equal(true)
Placeholder(Atom(1), Atom(2)).isBindingCompatible(Set(Variable("x", 40))) should equal(true)
}
}
package pl.edu.agh.kis.eis
import org.scalatest.{FlatSpec, Matchers}
class VarOrAtomSpec extends FlatSpec with Matchers {
val knownBindings: Seq[Set[Variable[Int]]] = Seq(
Set(Variable("x", 1), Variable("y", 2)),
Set(Variable("x", 2), Variable("y", 1))
)
"Adding compatible binding" should "return known input bindings back" in {
Variable.augmentBindings(knownBindings, Set(Variable[Int]("x", 1))) should equal(Seq(knownBindings.head))
Variable.augmentBindings(knownBindings, Set(Variable[Int]("y", 1))) should equal(Seq(knownBindings(1)))
}
"Adding non-existent variable" should "augment existing bindings with new element" in {
Variable.augmentBindings(knownBindings, Set(Variable[Int]("z", 3))) should equal(Seq[Set[Variable[Int]]](
Set(Variable("x", 1), Variable("y", 2), Variable[Int]("z", 3)),
Set(Variable("x", 2), Variable("y", 1), Variable[Int]("z", 3))
))
}
"Adding conflicting binding should" should "discard whole binding" in {
Variable.augmentBindings(knownBindings, Set(Variable[Int]("x", 10))) should be(empty)
Variable.augmentBindings(knownBindings, Set(Variable[Int]("y", 20))) should be(empty)
}
"Bindings" should "be augmented when there are no known bindings" in {
val candidateBindings: Set[Variable[Int]] = Set(Variable("x", 1), Variable("y", 2))
Variable.augmentBindings(Seq.empty, candidateBindings) should equal(Seq(candidateBindings))
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment