diff options
| -rw-r--r-- | src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | 266 |
1 files changed, 159 insertions, 107 deletions
diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala index 763f4f5..2e055b9 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | |||
| @@ -21,7 +21,6 @@ import org.semanticweb.owlapi.model.{ | |||
| 21 | } | 21 | } |
| 22 | import org.semanticweb.owlapi.model.parameters.Imports | 22 | import org.semanticweb.owlapi.model.parameters.Imports |
| 23 | import org.semanticweb.owlapi.reasoner.structural.StructuralReasonerFactory | 23 | import org.semanticweb.owlapi.reasoner.structural.StructuralReasonerFactory |
| 24 | import org.semanticweb.owlapi.model.{IRI => OWLIRI} | ||
| 25 | import uk.ac.manchester.cs.owl.owlapi.OWLObjectPropertyImpl | 24 | import uk.ac.manchester.cs.owl.owlapi.OWLObjectPropertyImpl |
| 26 | 25 | ||
| 27 | import tech.oxfordsemantic.jrdfox.client.{ | 26 | import tech.oxfordsemantic.jrdfox.client.{ |
| @@ -48,9 +47,10 @@ import tech.oxfordsemantic.jrdfox.logic.sparql.statement.SelectQuery | |||
| 48 | /* Scala imports */ | 47 | /* Scala imports */ |
| 49 | import scala.util.{Try, Success, Failure} | 48 | import scala.util.{Try, Success, Failure} |
| 50 | import scala.collection.JavaConverters._ | 49 | import scala.collection.JavaConverters._ |
| 51 | import scala.collection.mutable.Set | 50 | import scala.collection.mutable.{Set, Map} |
| 52 | import scalax.collection.immutable.Graph | 51 | import scalax.collection.Graph |
| 53 | import scalax.collection.GraphEdge.UnDiEdge | 52 | import scalax.collection.GraphPredef._, scalax.collection.GraphEdge._ |
| 53 | import scalax.collection.GraphTraversal._ | ||
| 54 | 54 | ||
| 55 | /* Debug only */ | 55 | /* Debug only */ |
| 56 | import org.semanticweb.owlapi.dlsyntax.renderer.DLSyntaxObjectRenderer | 56 | import org.semanticweb.owlapi.dlsyntax.renderer.DLSyntaxObjectRenderer |
| @@ -170,103 +170,6 @@ class RSAOntology(_ontology: File, val datafiles: File*) { | |||
| 170 | .flatMap(_.objectPropertyExpressionsInSignature) | 170 | .flatMap(_.objectPropertyExpressionsInSignature) |
| 171 | .distinct | 171 | .distinct |
| 172 | 172 | ||
| 173 | /* Steps for RSA check | ||
| 174 | * 1) convert ontology axioms into LP rules | ||
| 175 | * 2) call RDFox on the onto and compute materialization | ||
| 176 | * 3) build graph from E(x,y) facts | ||
| 177 | * 4) check if the graph is tree-like | ||
| 178 | * ideally this annotates the graph with info about the reasons | ||
| 179 | * why the ontology might not be RSA. This could help a second | ||
| 180 | * step of approximation of an Horn-ALCHOIQ to RSA | ||
| 181 | * | ||
| 182 | * TODO: Implement additional checks (taking into account equality) | ||
| 183 | * | ||
| 184 | * To check if the graph is tree-like we check for acyclicity in a | ||
| 185 | * undirected graph. | ||
| 186 | */ | ||
| 187 | lazy val isRSA: Boolean = Logger.timed( | ||
| 188 | { | ||
| 189 | val unsafe = this.unsafeRoles | ||
| 190 | |||
| 191 | object RSAConverter extends RDFoxConverter { | ||
| 192 | |||
| 193 | override def convert( | ||
| 194 | expr: OWLClassExpression, | ||
| 195 | term: Term, | ||
| 196 | unsafe: List[OWLObjectPropertyExpression], | ||
| 197 | skolem: SkolemStrategy, | ||
| 198 | suffix: RSASuffix | ||
| 199 | ): Shards = | ||
| 200 | (expr, skolem) match { | ||
| 201 | |||
| 202 | case (e: OWLObjectSomeValuesFrom, c: Constant) | ||
| 203 | if unsafe contains e.getProperty => { | ||
| 204 | val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) | ||
| 205 | (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) | ||
| 206 | } | ||
| 207 | |||
| 208 | case (e: OWLDataSomeValuesFrom, c: Constant) | ||
| 209 | if unsafe contains e.getProperty => { | ||
| 210 | val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) | ||
| 211 | (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) | ||
| 212 | } | ||
| 213 | |||
| 214 | case _ => super.convert(expr, term, unsafe, skolem, suffix) | ||
| 215 | } | ||
| 216 | } | ||
| 217 | |||
| 218 | /* Ontology convertion into LP rules */ | ||
| 219 | val term = RSAOntology.genFreshVariable() | ||
| 220 | val conversion = Try( | ||
| 221 | axioms.map(a => | ||
| 222 | RSAConverter.convert(a, term, unsafe, new Constant(a), Empty) | ||
| 223 | ) | ||
| 224 | ) | ||
| 225 | |||
| 226 | conversion match { | ||
| 227 | case Success(result) => { | ||
| 228 | val datalog = result.unzip | ||
| 229 | val facts = datalog._1.flatten | ||
| 230 | var rules = datalog._2.flatten | ||
| 231 | |||
| 232 | /* Open connection with RDFox */ | ||
| 233 | val (server, data) = RDFoxUtil.openConnection("RSACheck") | ||
| 234 | |||
| 235 | /* Add additional built-in rules */ | ||
| 236 | val varX = Variable.create("X") | ||
| 237 | val varY = Variable.create("Y") | ||
| 238 | rules = Rule.create( | ||
| 239 | RSA.E(varX, varY), | ||
| 240 | RSA.PE(varX, varY), | ||
| 241 | RSA.U(varX), | ||
| 242 | RSA.U(varY) | ||
| 243 | ) :: rules | ||
| 244 | |||
| 245 | /* Load facts and rules from ontology */ | ||
| 246 | RDFoxUtil.addFacts(data, facts) | ||
| 247 | RDFoxUtil.addRules(data, rules) | ||
| 248 | /* Load data files */ | ||
| 249 | RDFoxUtil.addData(data, datafiles: _*) | ||
| 250 | |||
| 251 | /* Build graph */ | ||
| 252 | val graph = this.rsaGraph(data); | ||
| 253 | |||
| 254 | /* Close connection to RDFox */ | ||
| 255 | RDFoxUtil.closeConnection(server, data) | ||
| 256 | |||
| 257 | /* Acyclicity test */ | ||
| 258 | graph.isAcyclic | ||
| 259 | } | ||
| 260 | case Failure(e) => { | ||
| 261 | Logger print s"Unsupported axiom: $e" | ||
| 262 | false | ||
| 263 | } | ||
| 264 | } | ||
| 265 | }, | ||
| 266 | "RSA check", | ||
| 267 | Logger.DEBUG | ||
| 268 | ) | ||
| 269 | |||
| 270 | /** Unsafe roles of a given ontology. | 173 | /** Unsafe roles of a given ontology. |
| 271 | * | 174 | * |
| 272 | * Unsafety conditions are the following: | 175 | * Unsafety conditions are the following: |
| @@ -321,16 +224,165 @@ class RSAOntology(_ontology: File, val datafiles: File*) { | |||
| 321 | (unsafe1 ++ unsafe2).toList | 224 | (unsafe1 ++ unsafe2).toList |
| 322 | } | 225 | } |
| 323 | 226 | ||
| 324 | private def rsaGraph( | 227 | /** RSA dependency graph |
| 325 | data: DataStoreConnection | 228 | * |
| 326 | ): Graph[Resource, UnDiEdge] = { | 229 | * This is used to check whether the input ontology is RSA. This also |
| 230 | * helps us determine a suitable approximation of the ontology to | ||
| 231 | * RSA. | ||
| 232 | */ | ||
| 233 | private lazy val dependencyGraph: Graph[Resource, DiEdge] = { | ||
| 234 | val unsafe = this.unsafeRoles | ||
| 235 | var nodemap = Map.empty[String, OWLAxiom] | ||
| 236 | |||
| 237 | object RSAConverter extends RDFoxConverter { | ||
| 238 | |||
| 239 | override def convert( | ||
| 240 | expr: OWLClassExpression, | ||
| 241 | term: Term, | ||
| 242 | unsafe: List[OWLObjectPropertyExpression], | ||
| 243 | skolem: SkolemStrategy, | ||
| 244 | suffix: RSASuffix | ||
| 245 | ): Shards = | ||
| 246 | (expr, skolem) match { | ||
| 247 | |||
| 248 | case (e: OWLObjectSomeValuesFrom, c: Constant) => { | ||
| 249 | nodemap.update(c.iri.getIRI, c.axiom) | ||
| 250 | val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) | ||
| 251 | if (unsafe contains e.getProperty) | ||
| 252 | (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) | ||
| 253 | else | ||
| 254 | (RSA.PE(term, c.iri) :: res, ext) | ||
| 255 | } | ||
| 256 | |||
| 257 | case (e: OWLDataSomeValuesFrom, c: Constant) => { | ||
| 258 | nodemap.update(c.iri.getIRI, c.axiom) | ||
| 259 | val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) | ||
| 260 | if (unsafe contains e.getProperty) | ||
| 261 | (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) | ||
| 262 | else | ||
| 263 | (RSA.PE(term, c.iri) :: res, ext) | ||
| 264 | } | ||
| 265 | |||
| 266 | case _ => super.convert(expr, term, unsafe, skolem, suffix) | ||
| 267 | } | ||
| 268 | } | ||
| 269 | |||
| 270 | /* Ontology convertion into LP rules */ | ||
| 271 | val term = RSAOntology.genFreshVariable() | ||
| 272 | val result = axioms.map(a => | ||
| 273 | RSAConverter.convert(a, term, unsafe, new Constant(a), Empty) | ||
| 274 | ) | ||
| 275 | |||
| 276 | val datalog = result.unzip | ||
| 277 | val facts = datalog._1.flatten | ||
| 278 | var rules = datalog._2.flatten | ||
| 279 | |||
| 280 | /* Open connection with RDFox */ | ||
| 281 | val (server, data) = RDFoxUtil.openConnection("RSACheck") | ||
| 282 | |||
| 283 | /* Add additional built-in rules */ | ||
| 284 | val varX = Variable.create("X") | ||
| 285 | val varY = Variable.create("Y") | ||
| 286 | rules = Rule.create( | ||
| 287 | RSA.E(varX, varY), | ||
| 288 | RSA.PE(varX, varY), | ||
| 289 | RSA.U(varX), | ||
| 290 | RSA.U(varY) | ||
| 291 | ) :: rules | ||
| 292 | |||
| 293 | /* Load facts and rules from ontology */ | ||
| 294 | RDFoxUtil.addFacts(data, facts) | ||
| 295 | RDFoxUtil.addRules(data, rules) | ||
| 296 | /* Load data files */ | ||
| 297 | RDFoxUtil.addData(data, datafiles: _*) | ||
| 298 | |||
| 299 | /* Build graph */ | ||
| 327 | val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" | 300 | val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" |
| 328 | val answers = RDFoxUtil.submitQuery(data, query, RSA.Prefixes).get | 301 | val answers = RDFoxUtil.submitQuery(data, query, RSA.Prefixes).get |
| 329 | var edges: Seq[UnDiEdge[Resource]] = | 302 | var edges: Seq[DiEdge[Resource]] = |
| 330 | answers.collect { case (_, Seq(n1, n2)) => UnDiEdge(n1, n2) } | 303 | answers.collect { case (_, Seq(n1, n2)) => n1 ~> n2 } |
| 331 | Graph(edges: _*) | 304 | val graph = Graph(edges: _*) |
| 305 | |||
| 306 | /* Close connection to RDFox */ | ||
| 307 | RDFoxUtil.closeConnection(server, data) | ||
| 308 | |||
| 309 | /* Approximate the ontology to RSA */ | ||
| 310 | approximate(graph, nodemap) | ||
| 311 | |||
| 312 | graph | ||
| 332 | } | 313 | } |
| 333 | 314 | ||
| 315 | /** Approximate a Horn-ALCHOIQ ontology to RSA | ||
| 316 | * | ||
| 317 | * This is done by gathering those axioms that prevent the ontology | ||
| 318 | * dependency graph `dependencyGraph` from being tree-shaped, and | ||
| 319 | * removing them. | ||
| 320 | * | ||
| 321 | * @param graph the graph used to compute the axioms to remove. | ||
| 322 | * @param nodemap map from graph nodes to ontology axioms. | ||
| 323 | */ | ||
| 324 | private def approximate( | ||
| 325 | graph: Graph[Resource, DiEdge], | ||
| 326 | nodemap: Map[String, OWLAxiom] | ||
| 327 | ): Unit = { | ||
| 328 | |||
| 329 | /* Define node colors for the graph visit */ | ||
| 330 | sealed trait NodeColor | ||
| 331 | case object Unvisited extends NodeColor | ||
| 332 | case object Visited extends NodeColor | ||
| 333 | case object ToDelete extends NodeColor | ||
| 334 | |||
| 335 | /* Keep track of node colors during graph visit */ | ||
| 336 | var color = Map.from[Resource, NodeColor]( | ||
| 337 | graph.nodes.toOuter.map(k => (k, Unvisited)) | ||
| 338 | ) | ||
| 339 | |||
| 340 | for { | ||
| 341 | component <- graph.componentTraverser().map(_ to Graph) | ||
| 342 | edge <- component | ||
| 343 | .outerEdgeTraverser(component.nodes.head) | ||
| 344 | .withKind(BreadthFirst) | ||
| 345 | } yield { | ||
| 346 | val source = edge._1 | ||
| 347 | val target = edge._2 | ||
| 348 | color(source) match { | ||
| 349 | case Unvisited | Visited => { | ||
| 350 | color(target) match { | ||
| 351 | case Unvisited => | ||
| 352 | color(source) = Visited; | ||
| 353 | color(target) = Visited | ||
| 354 | case Visited => | ||
| 355 | color(source) = ToDelete | ||
| 356 | case ToDelete => | ||
| 357 | color(source) = Visited | ||
| 358 | } | ||
| 359 | } | ||
| 360 | case ToDelete => | ||
| 361 | } | ||
| 362 | } | ||
| 363 | |||
| 364 | val toDelete = color.iterator.collect { case (resource: IRI, ToDelete) => | ||
| 365 | nodemap(resource.getIRI) | ||
| 366 | }.toSeq | ||
| 367 | ontology.removeAxioms(toDelete: _*) | ||
| 368 | } | ||
| 369 | // val edges1 = Seq('A ~> 'B, 'B ~> 'C, 'C ~> 'D, 'D ~> 'H, 'H ~> | ||
| 370 | // 'G, 'G ~> 'F, 'E ~> 'A, 'E ~> 'F, 'B ~> 'E, 'F ~> 'G, 'B ~> 'F, | ||
| 371 | // 'C ~> 'G, 'D ~> 'C, 'H ~> 'D) | ||
| 372 | // val edges2 = Seq('I ~> 'M, 'I ~> 'L, 'L ~> 'N, 'M ~> 'N) | ||
| 373 | // val edges3 = Seq('P ~> 'O) | ||
| 374 | // val graph = Graph.from(edges = edges1 ++ edges2 ++ edges3) | ||
| 375 | |||
| 376 | /** RSA check | ||
| 377 | * | ||
| 378 | * Acyclicity check over *undirected* dependency graph. | ||
| 379 | * NOTE: at the moment we are using the direct version of the graph. | ||
| 380 | * | ||
| 381 | * @deprecated | ||
| 382 | */ | ||
| 383 | lazy val isRSA: Boolean = | ||
| 384 | Logger.timed(dependencyGraph.isAcyclic, "RSA check", Logger.DEBUG) | ||
| 385 | |||
| 334 | /** Top axiomatization rules | 386 | /** Top axiomatization rules |
| 335 | * | 387 | * |
| 336 | * For each concept/role *in the ontology file* introduce a rule to | 388 | * For each concept/role *in the ontology file* introduce a rule to |
