From e1a04294ed8737444e40323474f4084cb64c1d55 Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Tue, 17 Nov 2020 14:19:08 +0000 Subject: Remove implicit RSAOntology conversion This was causing problems without giving any significant advantage. Now it will be easier to refactor classes like RSA, RSAOntology, CanonicalModel. --- src/main/scala/rsacomb/CanonicalModel.scala | 292 +++++++ src/main/scala/rsacomb/Main.scala | 2 +- src/main/scala/rsacomb/RDFoxUtil.scala | 10 + src/main/scala/rsacomb/RSA.scala | 11 +- src/main/scala/rsacomb/RSAOntology.scala | 951 +++++++-------------- src/test/scala/rsacomb/CanonicalModelSpec.scala | 22 +- src/test/scala/rsacomb/FilteringProgramSpecs.scala | 1 - 7 files changed, 624 insertions(+), 665 deletions(-) create mode 100644 src/main/scala/rsacomb/CanonicalModel.scala (limited to 'src') diff --git a/src/main/scala/rsacomb/CanonicalModel.scala b/src/main/scala/rsacomb/CanonicalModel.scala new file mode 100644 index 0000000..f7a45a7 --- /dev/null +++ b/src/main/scala/rsacomb/CanonicalModel.scala @@ -0,0 +1,292 @@ +package rsacomb + +import org.semanticweb.owlapi.model.{OWLObjectInverseOf, OWLObjectProperty} +import org.semanticweb.owlapi.model.{ + OWLClass, + // OWLObjectProperty, + OWLSubObjectPropertyOfAxiom, + // OWLObjectPropertyExpression, + OWLObjectSomeValuesFrom, + OWLSubClassOfAxiom +} + +import tech.oxfordsemantic.jrdfox.logic.datalog.{ + Rule, + BodyFormula, + TupleTableAtom, + Negation +} +import tech.oxfordsemantic.jrdfox.logic.expression.{ + Term, + Variable, + // Resource, + IRI +} + +import suffix.{Empty, Forward, Backward, Inverse} + +class CanonicalModel(val ontology: RSAOntology) extends RSAAxiom { + + // Makes working with IRIs less painful + import RDFoxUtil._ + + val named: List[Rule] = + ontology.individuals.map(a => + Rule.create(TupleTableAtom.rdf(a, IRI.RDF_TYPE, RSA.Named)) + ) + + val rolesAdditionalRules: List[Rule] = { + // Given a role (predicate) compute additional logic rules + def additional(pred: String): Seq[Rule] = { + val varX = Variable.create("X") + val varY = Variable.create("Y") + for ( + (hSuffix, bSuffix) <- List( + (Empty, Forward), + (Empty, Backward), + (Inverse, Forward + Inverse), + (Inverse, Backward + Inverse), + (Backward + Inverse, Forward), + (Forward + Inverse, Backward), + (Backward, Forward + Inverse), + (Forward, Backward + Inverse) + ) + ) + yield Rule.create( + TupleTableAtom.rdf(varX, pred :: hSuffix, varY), + TupleTableAtom.rdf(varX, pred :: bSuffix, varY) + ) + } + // Compute additional rules per role + ontology.roles + .collect { case prop: OWLObjectProperty => prop } + .map(_.getIRI.getIRIString) + .flatMap(additional) + } + + private lazy val topAxioms: List[Rule] = { + val varX = Variable.create("X") + val varY = Variable.create("Y") + val concepts = ontology.concepts.map(c => { + Rule.create( + TupleTableAtom.rdf(varX, IRI.RDF_TYPE, IRI.THING), + TupleTableAtom.rdf(varX, IRI.RDF_TYPE, c.getIRI) + ) + }) + val roles = ontology.roles.map(r => { + val name = r match { + case x: OWLObjectProperty => x.getIRI.getIRIString + case x: OWLObjectInverseOf => + x.getInverse.getNamedProperty.getIRI.getIRIString :: Inverse + } + Rule.create( + List( + TupleTableAtom.rdf(varX, IRI.RDF_TYPE, IRI.THING), + TupleTableAtom.rdf(varY, IRI.RDF_TYPE, IRI.THING) + ), + List(TupleTableAtom.rdf(varX, name, varY)) + ) + }) + concepts ::: roles + } + + private val equalityAxioms: List[Rule] = { + val varX = Variable.create("X") + val varY = Variable.create("Y") + val varZ = Variable.create("Z") + List( + Rule.create( + TupleTableAtom.rdf(varX, RSA.EquivTo, varX), + TupleTableAtom.rdf(varX, IRI.RDF_TYPE, IRI.THING) + ), + Rule.create( + TupleTableAtom.rdf(varY, RSA.EquivTo, varX), + TupleTableAtom.rdf(varX, RSA.EquivTo, varY) + ), + Rule.create( + TupleTableAtom.rdf(varX, RSA.EquivTo, varZ), + TupleTableAtom.rdf(varX, RSA.EquivTo, varY), + TupleTableAtom.rdf(varY, RSA.EquivTo, varZ) + ) + ) + } + + val rules: List[Rule] = { + // Compute rules from ontology axioms + val rules = ontology.axioms.flatMap(_.accept(RuleGenerator)) + // Return full set of rules + rules ::: rolesAdditionalRules ::: topAxioms ::: equalityAxioms ::: named + } + + object RuleGenerator + extends RDFoxAxiomConverter( + Variable.create("X"), + ontology.unsafeRoles, + SkolemStrategy.None, + Empty + ) { + + private def rules1(axiom: OWLSubClassOfAxiom): List[Rule] = { + val unfold = ontology.unfold(axiom).toList + // Fresh Variables + val v0 = RSA.rsa("v0_" ++ RSA.hashed(axiom)) + val varX = Variable.create("X") + // Predicates + val atomA: TupleTableAtom = { + val cls = axiom.getSubClass.asInstanceOf[OWLClass].getIRI + TupleTableAtom.rdf(varX, IRI.RDF_TYPE, cls) + } + def predIN(t: Term): TupleTableAtom = { + TupleTableAtom.rdf( + t, + RSA.rsa("IN"), + RSA.rsa(unfold.hashCode.toString) + ) + } + def notIn(t: Term): Negation = Negation.create(predIN(t)) + val roleRf: TupleTableAtom = { + val visitor = + new RDFoxPropertyExprConverter(varX, v0, Forward) + axiom.getSuperClass + .asInstanceOf[OWLObjectSomeValuesFrom] + .getProperty + .accept(visitor) + .head + } + val atomB: TupleTableAtom = { + val cls = axiom.getSuperClass + .asInstanceOf[OWLObjectSomeValuesFrom] + .getFiller + .asInstanceOf[OWLClass] + .getIRI + TupleTableAtom.rdf(v0, IRI.RDF_TYPE, cls) + } + // TODO: To be consistent with the specifics of the visitor we are + // returning facts as `Rule`s with true body. While this is correct + // there is an easier way to import facts into RDFox. Are we able to + // do that? + val facts = unfold.map(x => Rule.create(predIN(x))) + val rules = List( + Rule.create(roleRf, atomA, notIn(varX)), + Rule.create(atomB, atomA, notIn(varX)) + ) + facts ++ rules + } + + private def rules2(axiom: OWLSubClassOfAxiom): List[Rule] = { + val roleR = + axiom.getSuperClass + .asInstanceOf[OWLObjectSomeValuesFrom] + .getProperty + if (ontology.confl(roleR) contains roleR) { + // Fresh Variables + val v0 = RSA.rsa("v0_" ++ RSA.hashed(axiom)) + val v1 = RSA.rsa("v1_" ++ RSA.hashed(axiom)) + val v2 = RSA.rsa("v2_" ++ RSA.hashed(axiom)) + // Predicates + def atomA(t: Term): TupleTableAtom = { + val cls = axiom.getSubClass.asInstanceOf[OWLClass].getIRI + TupleTableAtom.rdf(t, IRI.RDF_TYPE, cls) + } + def roleRf(t1: Term, t2: Term): TupleTableAtom = { + val visitor = + new RDFoxPropertyExprConverter(t1, t2, Forward) + roleR.accept(visitor).head + } + def atomB(t: Term): TupleTableAtom = { + val cls = axiom.getSuperClass + .asInstanceOf[OWLObjectSomeValuesFrom] + .getFiller + .asInstanceOf[OWLClass] + .getIRI + TupleTableAtom.rdf(t, IRI.RDF_TYPE, cls) + } + //Rules + List( + Rule.create(roleRf(v0, v1), atomA(v0)), + Rule.create(atomB(v1), atomA(v0)), + Rule.create(roleRf(v1, v2), atomA(v1)), + Rule.create(atomB(v2), atomA(v1)) + ) + } else { + List() + } + } + + private def rules3(axiom: OWLSubClassOfAxiom): List[Rule] = { + val cycle = ontology.cycle(axiom).toList + val roleR = + axiom.getSuperClass + .asInstanceOf[OWLObjectSomeValuesFrom] + .getProperty + // Fresh Variables + val v1 = RSA.rsa("v1_" ++ RSA.hashed(axiom)) + // Predicates + def atomA(t: Term): TupleTableAtom = { + val cls = axiom.getSubClass.asInstanceOf[OWLClass].getIRI + TupleTableAtom.rdf(t, IRI.RDF_TYPE, cls) + } + def roleRf(t: Term): TupleTableAtom = { + val visitor = + new RDFoxPropertyExprConverter(t, v1, Forward) + roleR.accept(visitor).head + } + val atomB: TupleTableAtom = { + val cls = axiom.getSuperClass + .asInstanceOf[OWLObjectSomeValuesFrom] + .getFiller + .asInstanceOf[OWLClass] + .getIRI + TupleTableAtom.rdf(v1, IRI.RDF_TYPE, cls) + } + cycle.flatMap { x => + List( + Rule.create(roleRf(x), atomA(x)), + Rule.create(atomB, atomA(x)) + ) + } + } + + override def visit(axiom: OWLSubClassOfAxiom): List[Rule] = { + if (axiom.isT5) { + // TODO: get role in T5 axiom + // Assuming one role here + val role = axiom.objectPropertyExpressionsInSignature(0) + if (ontology.unsafeRoles contains role) { + val visitor = + new RDFoxAxiomConverter( + Variable.create("X"), + ontology.unsafeRoles, + SkolemStrategy.Standard(axiom.toString), + Forward + ) + axiom.accept(visitor) + } else { + rules1(axiom) ::: rules2(axiom) ::: rules3(axiom) + } + } else { + // Fallback to standard OWL to LP translation + super.visit(axiom) + } + } + + override def visit(axiom: OWLSubObjectPropertyOfAxiom): List[Rule] = { + val varX = Variable.create("X") + val visitorF = new RDFoxAxiomConverter( + varX, + ontology.unsafeRoles, + SkolemStrategy.None, + Forward + ) + val visitorB = new RDFoxAxiomConverter( + varX, + ontology.unsafeRoles, + SkolemStrategy.None, + Backward + ) + axiom.accept(visitorB) ::: axiom.accept(visitorF) + } + + } + +} diff --git a/src/main/scala/rsacomb/Main.scala b/src/main/scala/rsacomb/Main.scala index 3963981..8270205 100644 --- a/src/main/scala/rsacomb/Main.scala +++ b/src/main/scala/rsacomb/Main.scala @@ -52,7 +52,7 @@ object RSAComb extends App { * case. */ - val ontology: RSAOntology = RSA.loadOntology(ontoPath) + val ontology = RSAOntology(ontoPath) if (ontology.isRSA) { /* Load query */ diff --git a/src/main/scala/rsacomb/RDFoxUtil.scala b/src/main/scala/rsacomb/RDFoxUtil.scala index d391d41..653c51f 100644 --- a/src/main/scala/rsacomb/RDFoxUtil.scala +++ b/src/main/scala/rsacomb/RDFoxUtil.scala @@ -15,6 +15,8 @@ import tech.oxfordsemantic.jrdfox.formats.SPARQLParser import tech.oxfordsemantic.jrdfox.logic.expression.{IRI => RDFox_IRI} import org.semanticweb.owlapi.model.{IRI => OWL_IRI} +import scala.collection.JavaConverters._ + object RDFoxUtil { implicit def rdfox2owlapi(iri: RDFox_IRI): OWL_IRI = { @@ -29,6 +31,14 @@ object RDFoxUtil { RDFox_IRI.create(iri) } + implicit def javaToScalaList[A](list: java.util.List[A]): List[A] = { + list.asScala.toList + } + + implicit def scalaToJavaList[A](list: List[A]): java.util.List[A] = { + list.asJava + } + def openConnection( dataStore: String ): (ServerConnection, DataStoreConnection) = { diff --git a/src/main/scala/rsacomb/RSA.scala b/src/main/scala/rsacomb/RSA.scala index dd09899..b0e140b 100644 --- a/src/main/scala/rsacomb/RSA.scala +++ b/src/main/scala/rsacomb/RSA.scala @@ -1,13 +1,11 @@ package rsacomb /* Java imports */ -import java.io.File import java.util.Map import tech.oxfordsemantic.jrdfox.formats.SPARQLParser import tech.oxfordsemantic.jrdfox.Prefixes import tech.oxfordsemantic.jrdfox.logic.expression.{Variable, IRI} -import org.semanticweb.owlapi.apibinding.OWLManager import org.semanticweb.owlapi.model.OWLOntology import org.semanticweb.owlapi.model.{ OWLAxiom, @@ -18,7 +16,7 @@ import org.semanticweb.owlapi.model.{ // Debug only import scala.collection.JavaConverters._ -object RSA extends RSAOntology with RSAAxiom { +object RSA extends RSAAxiom { val Prefixes: Prefixes = new Prefixes() Prefixes.declarePrefix(":", "http://example.com/rsa_example.owl#") @@ -28,6 +26,7 @@ object RSA extends RSAOntology with RSAAxiom { Prefixes.declarePrefix("owl:", "http://www.w3.org/2002/07/owl#") val EquivTo: IRI = this.rsa("EquivTo") + val Named: IRI = this.rsa("NAMED") // Counter used to implement a simple fresh variable generator private var counter = -1; @@ -61,10 +60,4 @@ object RSA extends RSAOntology with RSAAxiom { this.hashed(cls1, prop, cls2) } - // TODO: move this somewhere else... maybe an OntoUtils class or something. - def loadOntology(onto: File): OWLOntology = { - val manager = OWLManager.createOWLOntologyManager() - manager.loadOntologyFromOntologyDocument(onto) - } - } // object RSA diff --git a/src/main/scala/rsacomb/RSAOntology.scala b/src/main/scala/rsacomb/RSAOntology.scala index dd65116..52bff37 100644 --- a/src/main/scala/rsacomb/RSAOntology.scala +++ b/src/main/scala/rsacomb/RSAOntology.scala @@ -3,6 +3,8 @@ package rsacomb /* Java imports */ import java.util.HashMap import java.util.stream.{Collectors, Stream} +import java.io.File +import org.semanticweb.owlapi.apibinding.OWLManager import org.semanticweb.owlapi.model.{OWLOntology, OWLAxiom} import org.semanticweb.owlapi.model.{ @@ -46,663 +48,326 @@ import org.semanticweb.owlapi.model.OWLObjectInverseOf import suffix.{Empty, Forward, Backward, Inverse} -object RSAOntology {} -/* Wrapper trait for the implicit class `RSAOntology`. - */ -trait RSAOntology { +object RSAOntology { - /* Implements additional features to reason about RSA ontologies - * on top of `OWLOntology` from the OWLAPI. - */ - implicit class RSAOntology(ontology: OWLOntology) extends RSAAxiom { - - // Gather TBox/RBox/ABox from original ontology - lazy val tbox: List[OWLAxiom] = - ontology - .tboxAxioms(Imports.INCLUDED) - .collect(Collectors.toList()) - .asScala - .toList - - lazy val rbox: List[OWLAxiom] = - ontology - .rboxAxioms(Imports.INCLUDED) - .collect(Collectors.toList()) - .asScala - .toList - - lazy val abox: List[OWLAxiom] = - ontology - .aboxAxioms(Imports.INCLUDED) - .collect(Collectors.toList()) - .asScala - .toList - - lazy val axioms: List[OWLAxiom] = abox ++ tbox ++ rbox - - /* Retrieve individuals in the original ontology - */ - lazy val individuals: List[IRI] = - ontology - .getIndividualsInSignature() - .asScala - .map(_.getIRI) - .map(RDFoxUtil.owlapi2rdfox) - .toList - - lazy val concepts: List[OWLClass] = - ontology.getClassesInSignature().asScala.toList - - lazy val roles: List[OWLObjectPropertyExpression] = - axioms - .flatMap(_.objectPropertyExpressionsInSignature) - .distinct - - // OWLAPI reasoner for same easier tasks - private val reasoner = - (new StructuralReasonerFactory()).createReasoner(ontology) - - /* Steps for RSA check - * 1) convert ontology axioms into LP rules - * 2) call RDFox on the onto and compute materialization - * 3) build graph from E(x,y) facts - * 4) check if the graph is tree-like - * ideally this annotates the graph with info about the reasons - * why the ontology might not be RSA. This could help a second - * step of approximation of an Horn-ALCHOIQ to RSA - */ - lazy val isRSA: Boolean = { - - val unsafe = this.unsafeRoles - - /* DEBUG: print rules in DL syntax and unsafe roles */ - //val renderer = new DLSyntaxObjectRenderer() - //println("\nDL rules:") - //axioms.foreach(x => println(renderer.render(x))) - //println("\nUnsafe roles:") - //println(unsafe) - - /* Ontology convertion into LP rules */ - val datalog = for { - axiom <- axioms - visitor = new RDFoxAxiomConverter( - RSA.getFreshVariable(), - unsafe, - SkolemStrategy.ConstantRSA(axiom.toString), - Empty - ) - rule <- axiom.accept(visitor) - } yield rule - - /* DEBUG: print datalog rules */ - //println("\nDatalog roles:") - //datalog.foreach(println) - - // Open connection with RDFox - val (server, data) = RDFoxUtil.openConnection("RSACheck") - // Add Data (hardcoded for now) - data.importData(UpdateType.ADDITION, RSA.Prefixes, ":a a :A .") - - /* Add built-in rules - */ - data.importData( - UpdateType.ADDITION, - RSA.Prefixes, - "[?X,?Y] :- [?X,?Y], [?X], [?Y] ." - ) + def apply(ontology: OWLOntology): RSAOntology = new RSAOntology(ontology) - /* Add built-in rules - */ - // data.importData( - // UpdateType.ADDITION, - // RSA.Prefixes, - // "[?entity, a, ?superClass] :- [?entity, a, ?class], [?class, rdfs:subClassOf, ?superClass] ." - // ) - - /* Add ontology rules - */ - data.addRules(datalog.asJava) - - /* Build graph - */ - val graph = this.rsaGraph(data); - //println(graph) - - // Close connection to RDFox - RDFoxUtil.closeConnection(server, data) - - /* To check if the graph is tree-like we check for acyclicity in a - * undirected graph. - * - * TODO: Implement additional checks (taking into account equality) - */ - graph.isAcyclic - } + def apply(ontology: File): RSAOntology = + new RSAOntology(loadOntology(ontology)) - lazy val unsafeRoles: List[OWLObjectPropertyExpression] = { - - /* DEBUG: print rules in DL syntax */ - //val renderer = new DLSyntaxObjectRenderer() - - /* Checking for (1) unsafety condition: - * - * For all roles r1 appearing in an axiom of type T5, r1 is unsafe - * if there exists a role r2 (different from top) appearing in an axiom - * of type T3 and r1 is a subproperty of the inverse of r2. - */ - val unsafe1 = for { - axiom <- tbox - if axiom.isT5 - role1 <- axiom.objectPropertyExpressionsInSignature - roleSuper = - role1 +: reasoner - .superObjectProperties(role1) - .collect(Collectors.toList()) - .asScala - roleSuperInv = roleSuper.map(_.getInverseProperty) - axiom <- tbox - if axiom.isT3 && !axiom.isT3top - role2 <- axiom.objectPropertyExpressionsInSignature - if roleSuperInv.contains(role2) - } yield role1 - - /* Checking for (2) unsafety condition: - * - * For all roles p1 appearing in an axiom of type T5, p1 is unsafe if - * there exists a role p2 appearing in an axiom of type T4 and p1 is a - * subproperty of either p2 or the inverse of p2. - * - */ - val unsafe2 = for { - axiom <- tbox - if axiom.isT5 - role1 <- axiom.objectPropertyExpressionsInSignature - roleSuper = - role1 +: reasoner - .superObjectProperties(role1) - .collect(Collectors.toList()) - .asScala - roleSuperInv = roleSuper.map(_.getInverseProperty) - axiom <- tbox - if axiom.isT4 - role2 <- axiom.objectPropertyExpressionsInSignature - if roleSuper.contains(role2) || roleSuperInv.contains(role2) - } yield role1 - - (unsafe1 ++ unsafe2).toList - } + private def loadOntology(onto: File): OWLOntology = { + val manager = OWLManager.createOWLOntologyManager() + manager.loadOntologyFromOntologyDocument(onto) + } +} - private def rsaGraph( - data: DataStoreConnection - ): Graph[Resource, UnDiEdge] = { - val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" - val cursor = - data.createCursor(RSA.Prefixes, query, new HashMap[String, String]()); - var mul = cursor.open() - var edges: List[UnDiEdge[Resource]] = List() - while (mul > 0) { - edges = UnDiEdge(cursor.getResource(0), cursor.getResource(1)) :: edges - mul = cursor.advance() - } - Graph(edges: _*) - } +class RSAOntology(val ontology: OWLOntology) extends RSAAxiom { + + // Gather TBox/RBox/ABox from original ontology + val tbox: List[OWLAxiom] = + ontology + .tboxAxioms(Imports.INCLUDED) + .collect(Collectors.toList()) + .asScala + .toList + + val rbox: List[OWLAxiom] = + ontology + .rboxAxioms(Imports.INCLUDED) + .collect(Collectors.toList()) + .asScala + .toList + + val abox: List[OWLAxiom] = + ontology + .aboxAxioms(Imports.INCLUDED) + .collect(Collectors.toList()) + .asScala + .toList + + val axioms: List[OWLAxiom] = abox ::: tbox ::: rbox + + /* Retrieve individuals in the original ontology + */ + val individuals: List[IRI] = + ontology + .getIndividualsInSignature() + .asScala + .map(_.getIRI) + .map(RDFoxUtil.owlapi2rdfox) + .toList + + val concepts: List[OWLClass] = + ontology.getClassesInSignature().asScala.toList + + val roles: List[OWLObjectPropertyExpression] = + axioms + .flatMap(_.objectPropertyExpressionsInSignature) + .distinct + + // OWLAPI reasoner for same easier tasks + private val reasoner = + (new StructuralReasonerFactory()).createReasoner(ontology) + + /* Steps for RSA check + * 1) convert ontology axioms into LP rules + * 2) call RDFox on the onto and compute materialization + * 3) build graph from E(x,y) facts + * 4) check if the graph is tree-like + * ideally this annotates the graph with info about the reasons + * why the ontology might not be RSA. This could help a second + * step of approximation of an Horn-ALCHOIQ to RSA + */ + lazy val isRSA: Boolean = { + + val unsafe = this.unsafeRoles + + /* DEBUG: print rules in DL syntax and unsafe roles */ + //val renderer = new DLSyntaxObjectRenderer() + //println("\nDL rules:") + //axioms.foreach(x => println(renderer.render(x))) + //println("\nUnsafe roles:") + //println(unsafe) + + /* Ontology convertion into LP rules */ + val datalog = for { + axiom <- axioms + visitor = new RDFoxAxiomConverter( + RSA.getFreshVariable(), + unsafe, + SkolemStrategy.ConstantRSA(axiom.toString), + Empty + ) + rule <- axiom.accept(visitor) + } yield rule - def filteringProgram( - query: SelectQuery, - nis: List[Term] - ): FilteringProgram = - new FilteringProgram(query, nis) - - // TODO: the following functions needs testing - def confl( - role: OWLObjectPropertyExpression - ): Set[OWLObjectPropertyExpression] = { - - val invSuperRoles = reasoner - .superObjectProperties(role) - .collect(Collectors.toSet()) - .asScala - .addOne(role) - .map(_.getInverseProperty) - - invSuperRoles - .flatMap(x => - reasoner - .subObjectProperties(x) - .collect(Collectors.toSet()) - .asScala - .addOne(x) - ) - .filterNot(_.isOWLBottomObjectProperty()) - .filterNot(_.getInverseProperty.isOWLTopObjectProperty()) - } + /* DEBUG: print datalog rules */ + //println("\nDatalog roles:") + //datalog.foreach(println) - def self(axiom: OWLSubClassOfAxiom): Set[Term] = { - // Assuming just one role in the signature of a T5 axiom - val role = axiom.objectPropertyExpressionsInSignature(0) - if (this.confl(role).contains(role)) { - Set( - RSA.rsa("v0_" ++ RSA.hashed(axiom)), - RSA.rsa("v1_" ++ RSA.hashed(axiom)) - ) - } else { - Set() - } - } + // Open connection with RDFox + val (server, data) = RDFoxUtil.openConnection("RSACheck") + // Add Data (hardcoded for now) + data.importData(UpdateType.ADDITION, RSA.Prefixes, ":a a :A .") - // def cycle(axiom: OWLSubClassOfAxiom): Set[Term] = { - // // Assuming just one role in the signature of a T5 axiom - // val roleR = axiom.objectPropertyExpressionsInSignature(0) - // val conflR = this.confl(roleR) - // // We just need the TBox to find - // val tbox = ontology - // .tboxAxioms(Imports.INCLUDED) - // .collect(Collectors.toSet()) - // .asScala - // for { - // axiom1 <- tbox - // // TODO: is this an optimization or an error? - // if axiom1.isT5 - // // We expect only one role coming out of a T5 axiom - // roleS <- axiom1.objectPropertyExpressionsInSignature - // // Triples ordering is among triples involving safe roles. - // if !unsafeRoles.contains(roleS) - // if conflR.contains(roleS) - // individual = - // if (axiom.hashCode < axiom1.hashCode) { - // RSA.rsa("v0_" ++ axiom1.hashCode.toString()) - // } else { - // RSA.rsa("v1_" ++ axiom1.hashCode.toString()) - // } - // } yield individual - // } - - def cycle(axiom: OWLSubClassOfAxiom): Set[Term] = { - // TODO: we can actually use `toTriple` from `RSAAxiom` - val classes = - axiom.classesInSignature.collect(Collectors.toList()).asScala - val classA = classes(0) - val roleR = axiom - .objectPropertyExpressionsInSignature(0) - .asInstanceOf[OWLObjectProperty] - val classB = classes(1) - cycle_aux(classA, roleR, classB) - } + /* Add built-in rules + */ + data.importData( + UpdateType.ADDITION, + RSA.Prefixes, + "[?X,?Y] :- [?X,?Y], [?X], [?Y] ." + ) - def cycle_aux( - classA: OWLClass, - roleR: OWLObjectProperty, - classB: OWLClass - ): Set[Term] = { - val conflR = this.confl(roleR) - val classes = ontology - .classesInSignature(Imports.INCLUDED) - .collect(Collectors.toSet()) - .asScala - for { - classD <- classes - roleS <- conflR - classC <- classes - // Keeping this check for now - if !unsafeRoles.contains(roleS) - tripleARB = RSA.hashed(classA, roleR, classB) - tripleDSC = RSA.hashed(classD, roleS, classC) - individual = - if (tripleARB > tripleDSC) { - RSA.rsa("v1_" ++ tripleDSC) - } else { - // Note that this is also the case for - // `tripleARB == tripleDSC` - RSA.rsa("v0_" ++ tripleDSC) - } - } yield individual - } + /* Add built-in rules + */ + // data.importData( + // UpdateType.ADDITION, + // RSA.Prefixes, + // "[?entity, a, ?superClass] :- [?entity, a, ?class], [?class, rdfs:subClassOf, ?superClass] ." + // ) - def unfold(axiom: OWLSubClassOfAxiom): Set[Term] = - this.self(axiom) | this.cycle(axiom) - - object canonicalModel { - - import RDFoxUtil._ - - val named: List[Rule] = - individuals.map(a => - Rule.create( - TupleTableAtom.rdf(a, IRI.RDF_TYPE, RSA.rsa("NAMED")) - ) - ) - - val rolesAdditionalRules: List[Rule] = { - // Given a role (predicate) compute additional logic rules - def additional(pred: String): Seq[Rule] = { - val varX = Variable.create("X") - val varY = Variable.create("Y") - List( - Rule.create( - TupleTableAtom.rdf(varX, IRI.create(pred), varY), - TupleTableAtom - .rdf( - varX, - IRI.create(pred :: Forward), - varY - ) - ), - Rule.create( - TupleTableAtom.rdf(varX, IRI.create(pred), varY), - TupleTableAtom - .rdf( - varX, - IRI.create(pred :: Backward), - varY - ) - ), - Rule.create( - TupleTableAtom.rdf(varX, IRI.create(pred ++ "_inv"), varY), - TupleTableAtom - .rdf( - varX, - IRI.create(pred :: Forward + Inverse), - varY - ) - ), - Rule.create( - TupleTableAtom.rdf(varX, IRI.create(pred ++ "_inv"), varY), - TupleTableAtom - .rdf( - varX, - IRI.create(pred :: Backward + Inverse), - varY - ) - ), - Rule.create( - TupleTableAtom.rdf( - varY, - IRI.create(pred :: Backward + Inverse), - varX - ), - TupleTableAtom - .rdf( - varX, - IRI.create(pred :: Forward), - varY - ) - ), - Rule.create( - TupleTableAtom.rdf( - varY, - IRI.create(pred :: Forward + Inverse), - varX - ), - TupleTableAtom.rdf( - varX, - IRI.create(pred :: Backward), - varY - ) - ), - Rule.create( - TupleTableAtom.rdf( - varY, - IRI.create(pred :: Backward), - varX - ), - TupleTableAtom - .rdf( - varX, - IRI.create(pred :: Forward + Inverse), - varY - ) - ), - Rule.create( - TupleTableAtom.rdf( - varY, - IRI.create(pred :: Forward), - varX - ), - TupleTableAtom.rdf( - varX, - IRI.create(pred :: Backward + Inverse), - varY - ) - ) - ) - } - // Compute additional rules per role - axioms - .flatMap( - _.objectPropertiesInSignature.collect(Collectors.toSet()).asScala - ) - .distinct - .map(_.getIRI.getIRIString) - .flatMap(additional) - } - - private lazy val topAxioms: List[Rule] = - concepts.map(c => { - val x = Variable.create("X") - Rule.create( - TupleTableAtom.rdf(x, IRI.RDF_TYPE, IRI.THING), - TupleTableAtom.rdf(x, IRI.RDF_TYPE, c.getIRI) - ) - }) ++ - roles.map(r => { - val x = Variable.create("X") - val y = Variable.create("Y") - val iri: IRI = r match { - case x: OWLObjectProperty => x.getIRI - case x: OWLObjectInverseOf => - IRI.create(x.getNamedProperty.getIRI.getIRIString ++ "_inv") - case x => x.getNamedProperty.getIRI - } - Rule.create( - List( - TupleTableAtom.rdf(x, IRI.RDF_TYPE, IRI.THING), - TupleTableAtom.rdf(y, IRI.RDF_TYPE, IRI.THING) - ).asJava, - List[BodyFormula](TupleTableAtom.rdf(x, iri, y)).asJava - ) - }) - - private val equalityAxioms: List[Rule] = { - val x = Variable.create("X") - val y = Variable.create("Y") - val z = Variable.create("Z") - List( - Rule.create( - TupleTableAtom.rdf(x, RSA.EquivTo, x), - TupleTableAtom.rdf(x, IRI.RDF_TYPE, IRI.THING) - ), - Rule.create( - TupleTableAtom.rdf(y, RSA.EquivTo, x), - TupleTableAtom.rdf(x, RSA.EquivTo, y) - ), - Rule.create( - TupleTableAtom.rdf(x, RSA.EquivTo, z), - TupleTableAtom.rdf(x, RSA.EquivTo, y), - TupleTableAtom.rdf(y, RSA.EquivTo, z) - ) - ) - } - - val rules: List[Rule] = { - // Compute rules from ontology axioms - val rules = axioms.flatMap(_.accept(this.ProgramGenerator)) - // Return full set of rules - rules ++ rolesAdditionalRules ++ topAxioms ++ equalityAxioms ++ named - } - - object ProgramGenerator - extends RDFoxAxiomConverter( - Variable.create("X"), - unsafeRoles, - SkolemStrategy.None, - Empty - ) { - - private def rules1(axiom: OWLSubClassOfAxiom): List[Rule] = { - val unfold = ontology.unfold(axiom).toList - // Fresh Variables - val v0 = RSA.rsa("v0_" ++ RSA.hashed(axiom)) - val varX = Variable.create("X") - // Predicates - val atomA: TupleTableAtom = { - val cls = axiom.getSubClass.asInstanceOf[OWLClass].getIRI - TupleTableAtom.rdf(varX, IRI.RDF_TYPE, cls) - } - def in(t: Term): TupleTableAtom = { - TupleTableAtom.rdf( - t, - RSA.rsa("IN"), - RSA.rsa(unfold.hashCode.toString) - ) - } - def notIn(t: Term): Negation = Negation.create(in(t)) - val roleRf: TupleTableAtom = { - val visitor = - new RDFoxPropertyExprConverter(varX, v0, Forward) - axiom.getSuperClass - .asInstanceOf[OWLObjectSomeValuesFrom] - .getProperty - .accept(visitor) - .head - } - val atomB: TupleTableAtom = { - val cls = axiom.getSuperClass - .asInstanceOf[OWLObjectSomeValuesFrom] - .getFiller - .asInstanceOf[OWLClass] - .getIRI - TupleTableAtom.rdf(v0, IRI.RDF_TYPE, cls) - } - // TODO: To be consistent with the specifics of the visitor we are - // returning facts as `Rule`s with true body. While this is correct - // there is an easier way to import facts into RDFox. Are we able to - // do that? - val facts = unfold.map(x => Rule.create(in(x))) - val rules = List( - Rule.create(roleRf, atomA, notIn(varX)), - Rule.create(atomB, atomA, notIn(varX)) - ) - facts ++ rules - } + /* Add ontology rules + */ + data.addRules(datalog.asJava) - private def rules2(axiom: OWLSubClassOfAxiom): List[Rule] = { - val roleR = - axiom.getSuperClass - .asInstanceOf[OWLObjectSomeValuesFrom] - .getProperty - if (ontology.confl(roleR) contains roleR) { - // Fresh Variables - val v0 = RSA.rsa("v0_" ++ RSA.hashed(axiom)) - val v1 = RSA.rsa("v1_" ++ RSA.hashed(axiom)) - val v2 = RSA.rsa("v2_" ++ RSA.hashed(axiom)) - // Predicates - def atomA(t: Term): TupleTableAtom = { - val cls = axiom.getSubClass.asInstanceOf[OWLClass].getIRI - TupleTableAtom.rdf(t, IRI.RDF_TYPE, cls) - } - def roleRf(t1: Term, t2: Term): TupleTableAtom = { - val visitor = - new RDFoxPropertyExprConverter(t1, t2, Forward) - roleR.accept(visitor).head - } - def atomB(t: Term): TupleTableAtom = { - val cls = axiom.getSuperClass - .asInstanceOf[OWLObjectSomeValuesFrom] - .getFiller - .asInstanceOf[OWLClass] - .getIRI - TupleTableAtom.rdf(t, IRI.RDF_TYPE, cls) - } - //Rules - List( - Rule.create(roleRf(v0, v1), atomA(v0)), - Rule.create(atomB(v1), atomA(v0)), - Rule.create(roleRf(v1, v2), atomA(v1)), - Rule.create(atomB(v2), atomA(v1)) - ) - } else { - List() - } - } + /* Build graph + */ + val graph = this.rsaGraph(data); + //println(graph) - private def rules3(axiom: OWLSubClassOfAxiom): List[Rule] = { - val cycle = ontology.cycle(axiom).toList - val roleR = - axiom.getSuperClass - .asInstanceOf[OWLObjectSomeValuesFrom] - .getProperty - // Fresh Variables - val v1 = RSA.rsa("v1_" ++ RSA.hashed(axiom)) - // Predicates - def atomA(t: Term): TupleTableAtom = { - val cls = axiom.getSubClass.asInstanceOf[OWLClass].getIRI - TupleTableAtom.rdf(t, IRI.RDF_TYPE, cls) - } - def roleRf(t: Term): TupleTableAtom = { - val visitor = - new RDFoxPropertyExprConverter(t, v1, Forward) - roleR.accept(visitor).head - } - val atomB: TupleTableAtom = { - val cls = axiom.getSuperClass - .asInstanceOf[OWLObjectSomeValuesFrom] - .getFiller - .asInstanceOf[OWLClass] - .getIRI - TupleTableAtom.rdf(v1, IRI.RDF_TYPE, cls) - } - cycle.flatMap { x => - List( - Rule.create(roleRf(x), atomA(x)), - Rule.create(atomB, atomA(x)) - ) - } - } + // Close connection to RDFox + RDFoxUtil.closeConnection(server, data) - override def visit(axiom: OWLSubClassOfAxiom): List[Rule] = { - if (axiom.isT5) { - // TODO: get role in T5 axiom - // Assuming one role here - val role = axiom.objectPropertyExpressionsInSignature(0) - if (ontology.unsafeRoles.contains(role)) { - val visitor = - new RDFoxAxiomConverter( - Variable.create("X"), - ontology.unsafeRoles, - SkolemStrategy.Standard(axiom.toString), - Forward - ) - axiom.accept(visitor) - } else { - rules1(axiom) ++ rules2(axiom) ++ rules3(axiom) - } - } else { - // Fallback to standard OWL to LP translation - super.visit(axiom) - } - } + /* To check if the graph is tree-like we check for acyclicity in a + * undirected graph. + * + * TODO: Implement additional checks (taking into account equality) + */ + graph.isAcyclic + } - override def visit(axiom: OWLSubObjectPropertyOfAxiom): List[Rule] = { - val varX = Variable.create("X") - val varY = Variable.create("Y") - val visitorF = new RDFoxAxiomConverter( - Variable.create("X"), - ontology.unsafeRoles, - SkolemStrategy.None, - Forward - ) - val visitorB = new RDFoxAxiomConverter( - Variable.create("X"), - ontology.unsafeRoles, - SkolemStrategy.None, - Backward - ) - axiom.accept(visitorB) ++ axiom.accept(visitorF) - } + lazy val unsafeRoles: List[OWLObjectPropertyExpression] = { - } + /* DEBUG: print rules in DL syntax */ + //val renderer = new DLSyntaxObjectRenderer() + /* Checking for (1) unsafety condition: + * + * For all roles r1 appearing in an axiom of type T5, r1 is unsafe + * if there exists a role r2 (different from top) appearing in an axiom + * of type T3 and r1 is a subproperty of the inverse of r2. + */ + val unsafe1 = for { + axiom <- tbox + if axiom.isT5 + role1 <- axiom.objectPropertyExpressionsInSignature + roleSuper = + role1 +: reasoner + .superObjectProperties(role1) + .collect(Collectors.toList()) + .asScala + roleSuperInv = roleSuper.map(_.getInverseProperty) + axiom <- tbox + if axiom.isT3 && !axiom.isT3top + role2 <- axiom.objectPropertyExpressionsInSignature + if roleSuperInv.contains(role2) + } yield role1 + + /* Checking for (2) unsafety condition: + * + * For all roles p1 appearing in an axiom of type T5, p1 is unsafe if + * there exists a role p2 appearing in an axiom of type T4 and p1 is a + * subproperty of either p2 or the inverse of p2. + * + */ + val unsafe2 = for { + axiom <- tbox + if axiom.isT5 + role1 <- axiom.objectPropertyExpressionsInSignature + roleSuper = + role1 +: reasoner + .superObjectProperties(role1) + .collect(Collectors.toList()) + .asScala + roleSuperInv = roleSuper.map(_.getInverseProperty) + axiom <- tbox + if axiom.isT4 + role2 <- axiom.objectPropertyExpressionsInSignature + if roleSuper.contains(role2) || roleSuperInv.contains(role2) + } yield role1 + + (unsafe1 ++ unsafe2).toList + } + + private def rsaGraph( + data: DataStoreConnection + ): Graph[Resource, UnDiEdge] = { + val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" + val cursor = + data.createCursor(RSA.Prefixes, query, new HashMap[String, String]()); + var mul = cursor.open() + var edges: List[UnDiEdge[Resource]] = List() + while (mul > 0) { + edges = UnDiEdge(cursor.getResource(0), cursor.getResource(1)) :: edges + mul = cursor.advance() + } + Graph(edges: _*) + } + + def filteringProgram( + query: SelectQuery, + nis: List[Term] + ): FilteringProgram = + new FilteringProgram(query, nis) + + lazy val canonicalModel = new CanonicalModel(this) + + // TODO: the following functions needs testing + def confl( + role: OWLObjectPropertyExpression + ): Set[OWLObjectPropertyExpression] = { + + val invSuperRoles = reasoner + .superObjectProperties(role) + .collect(Collectors.toSet()) + .asScala + .addOne(role) + .map(_.getInverseProperty) + + invSuperRoles + .flatMap(x => + reasoner + .subObjectProperties(x) + .collect(Collectors.toSet()) + .asScala + .addOne(x) + ) + .filterNot(_.isOWLBottomObjectProperty()) + .filterNot(_.getInverseProperty.isOWLTopObjectProperty()) + } + + def self(axiom: OWLSubClassOfAxiom): Set[Term] = { + // Assuming just one role in the signature of a T5 axiom + val role = axiom.objectPropertyExpressionsInSignature(0) + if (this.confl(role).contains(role)) { + Set( + RSA.rsa("v0_" ++ RSA.hashed(axiom)), + RSA.rsa("v1_" ++ RSA.hashed(axiom)) + ) + } else { + Set() } - } // implicit class RSAOntology + } + + // def cycle(axiom: OWLSubClassOfAxiom): Set[Term] = { + // // Assuming just one role in the signature of a T5 axiom + // val roleR = axiom.objectPropertyExpressionsInSignature(0) + // val conflR = this.confl(roleR) + // // We just need the TBox to find + // val tbox = ontology + // .tboxAxioms(Imports.INCLUDED) + // .collect(Collectors.toSet()) + // .asScala + // for { + // axiom1 <- tbox + // // TODO: is this an optimization or an error? + // if axiom1.isT5 + // // We expect only one role coming out of a T5 axiom + // roleS <- axiom1.objectPropertyExpressionsInSignature + // // Triples ordering is among triples involving safe roles. + // if !unsafeRoles.contains(roleS) + // if conflR.contains(roleS) + // individual = + // if (axiom.hashCode < axiom1.hashCode) { + // RSA.rsa("v0_" ++ axiom1.hashCode.toString()) + // } else { + // RSA.rsa("v1_" ++ axiom1.hashCode.toString()) + // } + // } yield individual + // } + + def cycle(axiom: OWLSubClassOfAxiom): Set[Term] = { + // TODO: we can actually use `toTriple` from `RSAAxiom` + val classes = + axiom.classesInSignature.collect(Collectors.toList()).asScala + val classA = classes(0) + val roleR = axiom + .objectPropertyExpressionsInSignature(0) + .asInstanceOf[OWLObjectProperty] + val classB = classes(1) + cycle_aux(classA, roleR, classB) + } + + def cycle_aux( + classA: OWLClass, + roleR: OWLObjectProperty, + classB: OWLClass + ): Set[Term] = { + val conflR = this.confl(roleR) + val classes = ontology + .classesInSignature(Imports.INCLUDED) + .collect(Collectors.toSet()) + .asScala + for { + classD <- classes + roleS <- conflR + classC <- classes + // Keeping this check for now + if !unsafeRoles.contains(roleS) + tripleARB = RSA.hashed(classA, roleR, classB) + tripleDSC = RSA.hashed(classD, roleS, classC) + individual = + if (tripleARB > tripleDSC) { + RSA.rsa("v1_" ++ tripleDSC) + } else { + // Note that this is also the case for + // `tripleARB == tripleDSC` + RSA.rsa("v0_" ++ tripleDSC) + } + } yield individual + } + + def unfold(axiom: OWLSubClassOfAxiom): Set[Term] = + this.self(axiom) | this.cycle(axiom) -} // trait RSAOntology +} // implicit class RSAOntology diff --git a/src/test/scala/rsacomb/CanonicalModelSpec.scala b/src/test/scala/rsacomb/CanonicalModelSpec.scala index ef27a4f..cac40a3 100644 --- a/src/test/scala/rsacomb/CanonicalModelSpec.scala +++ b/src/test/scala/rsacomb/CanonicalModelSpec.scala @@ -23,7 +23,7 @@ object Ontology1_CanonicalModelSpec { val renderer = new DLSyntaxObjectRenderer() val ontology_path: File = new File("examples/example1.ttl") - val ontology: RSAOntology = RSA.loadOntology(ontology_path) + val ontology = RSAOntology(ontology_path) val program = ontology.canonicalModel val roleR = new OWLObjectPropertyImpl(RSA.base("R")) @@ -87,7 +87,7 @@ class Ontology1_CanonicalModelSpec renderer.render(AsubClassOfD) should "be converted into a single Rule" in { val varX = Variable.create("X") - val visitor = program.ProgramGenerator + val visitor = program.RuleGenerator val rules = AsubClassOfD.accept(visitor) rules.loneElement shouldBe a[Rule] } @@ -152,7 +152,7 @@ class Ontology1_CanonicalModelSpec AsomeValuesFromSiC ) should "produce 1 rule" in { val varX = Variable.create("X") - val visitor = program.ProgramGenerator + val visitor = program.RuleGenerator val rules = AsomeValuesFromSiC.accept(visitor) rules should have length 1 } @@ -175,7 +175,7 @@ class Ontology1_CanonicalModelSpec // Rule 3 provides 48 rule (split in 2) // Then (1*2 + 48) + (0) + (48*2) = 146 val varX = Variable.create("X") - val visitor = program.ProgramGenerator + val visitor = program.RuleGenerator val rules = DsomeValuesFromRB.accept(visitor) rules should have length 146 } @@ -198,7 +198,7 @@ class Ontology1_CanonicalModelSpec // Rule 3 provides 32 rule (split in 2) // Then (1*2 + 32) + (0) + (32*2) = 98 val varX = Variable.create("X") - val visitor = program.ProgramGenerator + val visitor = program.RuleGenerator val rules = DsomeValuesFromRB.accept(visitor) rules should have length 146 } @@ -207,7 +207,7 @@ class Ontology1_CanonicalModelSpec SsubPropertyOfT ) should "produce 2 rules" in { val varX = Variable.create("X") - val visitor = program.ProgramGenerator + val visitor = program.RuleGenerator val rules = SsubPropertyOfT.accept(visitor) rules should have length 2 } @@ -220,7 +220,7 @@ object Ontology2_CanonicalModelSpec { val renderer = new DLSyntaxObjectRenderer() val ontology_path: File = new File("examples/example2.owl") - val ontology: RSAOntology = RSA.loadOntology(ontology_path) + val ontology = RSAOntology(ontology_path) val program = ontology.canonicalModel val roleR = new OWLObjectPropertyImpl(RSA.base("R")) @@ -326,7 +326,7 @@ class Ontology2_CanonicalModelSpec renderer.render( AsomeValuesFromRB ) should "produce 1 rule" in { - val visitor = program.ProgramGenerator + val visitor = program.RuleGenerator val rules = AsomeValuesFromRB.accept(visitor) rules should have length 1 } @@ -336,7 +336,7 @@ class Ontology2_CanonicalModelSpec renderer.render( BsomeValuesFromSC ) should "produce 1 rule" in { - val visitor = program.ProgramGenerator + val visitor = program.RuleGenerator val rules = BsomeValuesFromSC.accept(visitor) rules should have length 1 } @@ -346,7 +346,7 @@ class Ontology2_CanonicalModelSpec renderer.render( CsomeValuesFromTD ) should "produce 1 rule" in { - val visitor = program.ProgramGenerator + val visitor = program.RuleGenerator val rules = CsomeValuesFromTD.accept(visitor) rules should have length 1 } @@ -356,7 +356,7 @@ class Ontology2_CanonicalModelSpec renderer.render( DsomeValuesFromPA ) should "produce 1 rule" in { - val visitor = program.ProgramGenerator + val visitor = program.RuleGenerator val rules = DsomeValuesFromPA.accept(visitor) rules should have length 1 } diff --git a/src/test/scala/rsacomb/FilteringProgramSpecs.scala b/src/test/scala/rsacomb/FilteringProgramSpecs.scala index 26b0857..79e675f 100644 --- a/src/test/scala/rsacomb/FilteringProgramSpecs.scala +++ b/src/test/scala/rsacomb/FilteringProgramSpecs.scala @@ -283,7 +283,6 @@ object FilteringProgramSpec { val queries = List(query0, query1, query2, query3, query4, query5, query6, query7) - } class FilteringProgramSpec -- cgit v1.2.3