diff options
| author | Federico Igne <federico.igne@cs.ox.ac.uk> | 2021-05-31 15:06:47 +0100 |
|---|---|---|
| committer | Federico Igne <federico.igne@cs.ox.ac.uk> | 2021-05-31 15:06:47 +0100 |
| commit | e932527e33b6f4c1634995224188b26d870d92b2 (patch) | |
| tree | b84d9628e0308a5b1979149d025a821b5d8943ca | |
| parent | 7ab2d819947c03a6618e273d5996a171f0217119 (diff) | |
| download | RSAComb-e932527e33b6f4c1634995224188b26d870d92b2.tar.gz RSAComb-e932527e33b6f4c1634995224188b26d870d92b2.zip | |
Add scafolding for generic approximation support
3 files changed, 385 insertions, 143 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 c7b3bf0..e048c28 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | |||
| @@ -64,6 +64,104 @@ import uk.ac.ox.cs.rsacomb.sparql._ | |||
| 64 | import uk.ac.ox.cs.rsacomb.util.{RDFoxUtil, RSA} | 64 | import uk.ac.ox.cs.rsacomb.util.{RDFoxUtil, RSA} |
| 65 | import uk.ac.ox.cs.rsacomb.util.Logger | 65 | import uk.ac.ox.cs.rsacomb.util.Logger |
| 66 | 66 | ||
| 67 | object RSAUtil { | ||
| 68 | |||
| 69 | implicit def axiomsToOntology(axioms: Seq[OWLAxiom]) = { | ||
| 70 | val manager = OWLManager.createOWLOntologyManager() | ||
| 71 | manager.createOntology(axioms.asJava) | ||
| 72 | } | ||
| 73 | |||
| 74 | /** Compute the RSA dependency graph for a set of axioms | ||
| 75 | * | ||
| 76 | * @return a tuple containing the dependency graph and a map between | ||
| 77 | * the newly introduced constants and the corresponding input axioms. | ||
| 78 | * | ||
| 79 | * @note no check on the ontology language is performed since the | ||
| 80 | * construction of the dependency graph is computed regardless. The | ||
| 81 | * input axioms are assumed to be normalized. | ||
| 82 | */ | ||
| 83 | private def dependencyGraph( | ||
| 84 | axioms: Seq[OWLAxiom], | ||
| 85 | datafiles: Seq[File] | ||
| 86 | ): (Graph[Resource, DiEdge], Map[String, OWLAxiom]) = { | ||
| 87 | val unsafe = this.unsafeRoles | ||
| 88 | var nodemap = Map.empty[String, OWLAxiom] | ||
| 89 | |||
| 90 | object RSAConverter extends RDFoxConverter { | ||
| 91 | |||
| 92 | override def convert( | ||
| 93 | expr: OWLClassExpression, | ||
| 94 | term: Term, | ||
| 95 | unsafe: List[OWLObjectPropertyExpression], | ||
| 96 | skolem: SkolemStrategy, | ||
| 97 | suffix: RSASuffix | ||
| 98 | ): Shards = | ||
| 99 | (expr, skolem) match { | ||
| 100 | |||
| 101 | case (e: OWLObjectSomeValuesFrom, c: Constant) => { | ||
| 102 | nodemap.update(c.iri.getIRI, c.axiom) | ||
| 103 | val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) | ||
| 104 | if (unsafe contains e.getProperty) | ||
| 105 | (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) | ||
| 106 | else | ||
| 107 | (RSA.PE(term, c.iri) :: res, ext) | ||
| 108 | } | ||
| 109 | |||
| 110 | case (e: OWLDataSomeValuesFrom, c: Constant) => { | ||
| 111 | nodemap.update(c.iri.getIRI, c.axiom) | ||
| 112 | val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) | ||
| 113 | if (unsafe contains e.getProperty) | ||
| 114 | (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) | ||
| 115 | else | ||
| 116 | (RSA.PE(term, c.iri) :: res, ext) | ||
| 117 | } | ||
| 118 | |||
| 119 | case _ => super.convert(expr, term, unsafe, skolem, suffix) | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 123 | /* Ontology convertion into LP rules */ | ||
| 124 | val term = RSAOntology.genFreshVariable() | ||
| 125 | val result = axioms.map(a => | ||
| 126 | RSAConverter.convert(a, term, unsafe, new Constant(a), Empty) | ||
| 127 | ) | ||
| 128 | |||
| 129 | val datalog = result.unzip | ||
| 130 | val facts = datalog._1.flatten | ||
| 131 | var rules = datalog._2.flatten | ||
| 132 | |||
| 133 | /* Open connection with RDFox */ | ||
| 134 | val (server, data) = RDFoxUtil.openConnection("rsa_dependency_graph") | ||
| 135 | |||
| 136 | /* Add additional built-in rules */ | ||
| 137 | val varX = Variable.create("X") | ||
| 138 | val varY = Variable.create("Y") | ||
| 139 | rules = Rule.create( | ||
| 140 | RSA.E(varX, varY), | ||
| 141 | RSA.PE(varX, varY), | ||
| 142 | RSA.U(varX), | ||
| 143 | RSA.U(varY) | ||
| 144 | ) :: rules | ||
| 145 | /* Load facts and rules from ontology */ | ||
| 146 | RDFoxUtil.addFacts(data, facts) | ||
| 147 | RDFoxUtil.addRules(data, rules) | ||
| 148 | /* Load data files */ | ||
| 149 | RDFoxUtil.addData(data, datafiles: _*) | ||
| 150 | |||
| 151 | /* Build the graph */ | ||
| 152 | val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" | ||
| 153 | val answers = RDFoxUtil.submitQuery(data, query, RSA.Prefixes).get | ||
| 154 | var edges: Seq[DiEdge[Resource]] = | ||
| 155 | answers.collect { case (_, Seq(n1, n2)) => n1 ~> n2 } | ||
| 156 | val graph = Graph(edges: _*) | ||
| 157 | |||
| 158 | /* Close connection to RDFox */ | ||
| 159 | RDFoxUtil.closeConnection(server, data) | ||
| 160 | |||
| 161 | (graph, nodemap) | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
| 67 | object RSAOntology { | 165 | object RSAOntology { |
| 68 | 166 | ||
| 69 | /** Name of the RDFox data store used for CQ answering */ | 167 | /** Name of the RDFox data store used for CQ answering */ |
| @@ -79,14 +177,63 @@ object RSAOntology { | |||
| 79 | /** Manager instance to interface with OWLAPI */ | 177 | /** Manager instance to interface with OWLAPI */ |
| 80 | val manager = OWLManager.createOWLOntologyManager() | 178 | val manager = OWLManager.createOWLOntologyManager() |
| 81 | 179 | ||
| 82 | def apply(ontology: File, data: File*): RSAOntology = | 180 | def apply( |
| 181 | ontofile: File, | ||
| 182 | datafiles: Seq[File], | ||
| 183 | approx: Option[Approximation] = None | ||
| 184 | ): RSAOntology = { | ||
| 185 | val ontology = manager.loadOntologyFromOntologyDocument(ontofile) | ||
| 186 | RSAOntology(ontology, datafiles, approx) | ||
| 187 | } | ||
| 188 | |||
| 189 | def apply( | ||
| 190 | ontology: OWLOntology, | ||
| 191 | datafiles: Seq[File], | ||
| 192 | approx: Option[Approximation] = None | ||
| 193 | ): RSAOntology = { | ||
| 194 | val normalizer = new Normalizer() | ||
| 195 | |||
| 196 | /** TBox axioms */ | ||
| 197 | var tbox: List[OWLLogicalAxiom] = | ||
| 198 | original | ||
| 199 | .tboxAxioms(Imports.INCLUDED) | ||
| 200 | .collect(Collectors.toList()) | ||
| 201 | .collect { case a: OWLLogicalAxiom => a } | ||
| 202 | .flatMap(normalizer.normalize) | ||
| 203 | |||
| 204 | /** RBox axioms */ | ||
| 205 | var rbox: List[OWLLogicalAxiom] = | ||
| 206 | original | ||
| 207 | .rboxAxioms(Imports.INCLUDED) | ||
| 208 | .collect(Collectors.toList()) | ||
| 209 | .collect { case a: OWLLogicalAxiom => a } | ||
| 210 | .flatMap(normalizer.normalize) | ||
| 211 | |||
| 212 | /** ABox axioms | ||
| 213 | * | ||
| 214 | * @note this represents only the set of assertions contained in the | ||
| 215 | * ontology file. Data files specified in `datafiles` are directly | ||
| 216 | * imported in RDFox due to performance issues when trying to import | ||
| 217 | * large data files via OWLAPI. | ||
| 218 | */ | ||
| 219 | var abox: List[OWLLogicalAxiom] = | ||
| 220 | original | ||
| 221 | .aboxAxioms(Imports.INCLUDED) | ||
| 222 | .collect(Collectors.toList()) | ||
| 223 | .collect { case a: OWLLogicalAxiom => a } | ||
| 224 | .flatMap(normalizer.normalize) | ||
| 225 | |||
| 226 | /** Collection of logical axioms in the input ontology */ | ||
| 227 | var axioms: List[OWLLogicalAxiom] = abox ::: tbox ::: rbox | ||
| 228 | |||
| 83 | new RSAOntology( | 229 | new RSAOntology( |
| 84 | manager.loadOntologyFromOntologyDocument(ontology), | 230 | approx match { |
| 85 | data: _* | 231 | case Some(a) => a.approximate(axioms, datafiles) |
| 232 | case None => axioms | ||
| 233 | }, | ||
| 234 | datafiles | ||
| 86 | ) | 235 | ) |
| 87 | 236 | } | |
| 88 | def apply(ontology: OWLOntology, data: File*): RSAOntology = | ||
| 89 | new RSAOntology(ontology, data: _*) | ||
| 90 | } | 237 | } |
| 91 | 238 | ||
| 92 | /** Wrapper class for an ontology in RSA | 239 | /** Wrapper class for an ontology in RSA |
| @@ -94,7 +241,7 @@ object RSAOntology { | |||
| 94 | * @param ontology the input OWL2 ontology. | 241 | * @param ontology the input OWL2 ontology. |
| 95 | * @param datafiles additinal data (treated as part of the ABox) | 242 | * @param datafiles additinal data (treated as part of the ABox) |
| 96 | */ | 243 | */ |
| 97 | class RSAOntology(val original: OWLOntology, val datafiles: File*) { | 244 | class RSAOntology(val axioms: Seq[OWLAxiom], val datafiles: File*) { |
| 98 | 245 | ||
| 99 | /** Simplify conversion between OWLAPI and RDFox concepts */ | 246 | /** Simplify conversion between OWLAPI and RDFox concepts */ |
| 100 | import implicits.RDFox._ | 247 | import implicits.RDFox._ |
| @@ -104,49 +251,8 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) { | |||
| 104 | /** Set of axioms removed during the approximation to RSA */ | 251 | /** Set of axioms removed during the approximation to RSA */ |
| 105 | private var removed: Seq[OWLAxiom] = Seq.empty | 252 | private var removed: Seq[OWLAxiom] = Seq.empty |
| 106 | 253 | ||
| 107 | /** The normalizer normalizes the ontology and approximate it to | ||
| 108 | * Horn-ALCHOIQ. A further step is needed to obtain an RSA | ||
| 109 | * approximation of the input ontology `original`. | ||
| 110 | */ | ||
| 111 | private val normalizer = new Normalizer() | ||
| 112 | |||
| 113 | /** TBox axioms */ | ||
| 114 | var tbox: List[OWLLogicalAxiom] = | ||
| 115 | original | ||
| 116 | .tboxAxioms(Imports.INCLUDED) | ||
| 117 | .collect(Collectors.toList()) | ||
| 118 | .collect { case a: OWLLogicalAxiom => a } | ||
| 119 | .flatMap(normalizer.normalize) | ||
| 120 | |||
| 121 | /** RBox axioms */ | ||
| 122 | var rbox: List[OWLLogicalAxiom] = | ||
| 123 | original | ||
| 124 | .rboxAxioms(Imports.INCLUDED) | ||
| 125 | .collect(Collectors.toList()) | ||
| 126 | .collect { case a: OWLLogicalAxiom => a } | ||
| 127 | .flatMap(normalizer.normalize) | ||
| 128 | |||
| 129 | /** ABox axioms | ||
| 130 | * | ||
| 131 | * @note this represents only the set of assertions contained in the | ||
| 132 | * ontology file. Data files specified in `datafiles` are directly | ||
| 133 | * imported in RDFox due to performance issues when trying to import | ||
| 134 | * large data files via OWLAPI. | ||
| 135 | */ | ||
| 136 | var abox: List[OWLLogicalAxiom] = | ||
| 137 | original | ||
| 138 | .aboxAxioms(Imports.INCLUDED) | ||
| 139 | .collect(Collectors.toList()) | ||
| 140 | .collect { case a: OWLLogicalAxiom => a } | ||
| 141 | .flatMap(normalizer.normalize) | ||
| 142 | |||
| 143 | /** Collection of logical axioms in the input ontology */ | ||
| 144 | var axioms: List[OWLLogicalAxiom] = abox ::: tbox ::: rbox | ||
| 145 | |||
| 146 | /** Normalized Horn-ALCHOIQ ontology */ | 254 | /** Normalized Horn-ALCHOIQ ontology */ |
| 147 | val ontology = RSAOntology.manager.createOntology( | 255 | val ontology = RSAOntology.manager.createOntology(axioms.asJava) |
| 148 | axioms.asInstanceOf[List[OWLAxiom]].asJava | ||
| 149 | ) | ||
| 150 | 256 | ||
| 151 | /** OWLAPI internal reasoner instantiated over the approximated ontology */ | 257 | /** OWLAPI internal reasoner instantiated over the approximated ontology */ |
| 152 | private val reasoner = | 258 | private val reasoner = |
| @@ -170,7 +276,7 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) { | |||
| 170 | val concepts: List[OWLClass] = | 276 | val concepts: List[OWLClass] = |
| 171 | ontology.getClassesInSignature().asScala.toList | 277 | ontology.getClassesInSignature().asScala.toList |
| 172 | val roles: List[OWLObjectPropertyExpression] = | 278 | val roles: List[OWLObjectPropertyExpression] = |
| 173 | (tbox ++ rbox) | 279 | axioms |
| 174 | .flatMap(_.objectPropertyExpressionsInSignature) | 280 | .flatMap(_.objectPropertyExpressionsInSignature) |
| 175 | .distinct | 281 | .distinct |
| 176 | 282 | ||
| @@ -190,12 +296,12 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) { | |||
| 190 | 296 | ||
| 191 | /* Checking for unsafety condition (1) */ | 297 | /* Checking for unsafety condition (1) */ |
| 192 | val unsafe1 = for { | 298 | val unsafe1 = for { |
| 193 | axiom <- tbox | 299 | axiom <- axioms |
| 194 | if axiom.isT5 | 300 | if axiom.isT5 |
| 195 | role1 <- axiom.objectPropertyExpressionsInSignature | 301 | role1 <- axiom.objectPropertyExpressionsInSignature |
| 196 | roleSuper = role1 +: reasoner.superObjectProperties(role1) | 302 | roleSuper = role1 +: reasoner.superObjectProperties(role1) |
| 197 | roleSuperInv = roleSuper.map(_.getInverseProperty) | 303 | roleSuperInv = roleSuper.map(_.getInverseProperty) |
| 198 | axiom <- tbox | 304 | axiom <- axioms |
| 199 | if axiom.isT3 && !axiom.isT3top | 305 | if axiom.isT3 && !axiom.isT3top |
| 200 | role2 <- axiom.objectPropertyExpressionsInSignature | 306 | role2 <- axiom.objectPropertyExpressionsInSignature |
| 201 | if roleSuperInv contains role2 | 307 | if roleSuperInv contains role2 |
| @@ -203,12 +309,12 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) { | |||
| 203 | 309 | ||
| 204 | /* Checking for unsafety condition (2) */ | 310 | /* Checking for unsafety condition (2) */ |
| 205 | val unsafe2 = for { | 311 | val unsafe2 = for { |
| 206 | axiom <- tbox | 312 | axiom <- axioms |
| 207 | if axiom.isT5 | 313 | if axiom.isT5 |
| 208 | role1 <- axiom.objectPropertyExpressionsInSignature | 314 | role1 <- axiom.objectPropertyExpressionsInSignature |
| 209 | roleSuper = role1 +: reasoner.superObjectProperties(role1) | 315 | roleSuper = role1 +: reasoner.superObjectProperties(role1) |
| 210 | roleSuperInv = roleSuper.map(_.getInverseProperty) | 316 | roleSuperInv = roleSuper.map(_.getInverseProperty) |
| 211 | axiom <- tbox | 317 | axiom <- axioms |
| 212 | if axiom.isT4 | 318 | if axiom.isT4 |
| 213 | role2 <- axiom.objectPropertyExpressionsInSignature | 319 | role2 <- axiom.objectPropertyExpressionsInSignature |
| 214 | if roleSuper.contains(role2) || roleSuperInv.contains(role2) | 320 | if roleSuper.contains(role2) || roleSuperInv.contains(role2) |
| @@ -217,93 +323,6 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) { | |||
| 217 | unsafe1 ++ unsafe2 | 323 | unsafe1 ++ unsafe2 |
| 218 | } | 324 | } |
| 219 | 325 | ||
| 220 | /** Compute the RSA dependency graph | ||
| 221 | * | ||
| 222 | * This is used to approximate the input ontology to RSA. | ||
| 223 | * | ||
| 224 | * @return a tuple containing the dependency graph and a map between | ||
| 225 | * the constants newly introduced and the corresponding axioms in the | ||
| 226 | * ontology. | ||
| 227 | */ | ||
| 228 | private def dependencyGraph() | ||
| 229 | : (Graph[Resource, DiEdge], Map[String, OWLAxiom]) = { | ||
| 230 | val unsafe = this.unsafeRoles | ||
| 231 | var nodemap = Map.empty[String, OWLAxiom] | ||
| 232 | |||
| 233 | object RSAConverter extends RDFoxConverter { | ||
| 234 | |||
| 235 | override def convert( | ||
| 236 | expr: OWLClassExpression, | ||
| 237 | term: Term, | ||
| 238 | unsafe: List[OWLObjectPropertyExpression], | ||
| 239 | skolem: SkolemStrategy, | ||
| 240 | suffix: RSASuffix | ||
| 241 | ): Shards = | ||
| 242 | (expr, skolem) match { | ||
| 243 | |||
| 244 | case (e: OWLObjectSomeValuesFrom, c: Constant) => { | ||
| 245 | nodemap.update(c.iri.getIRI, c.axiom) | ||
| 246 | val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) | ||
| 247 | if (unsafe contains e.getProperty) | ||
| 248 | (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) | ||
| 249 | else | ||
| 250 | (RSA.PE(term, c.iri) :: res, ext) | ||
| 251 | } | ||
| 252 | |||
| 253 | case (e: OWLDataSomeValuesFrom, c: Constant) => { | ||
| 254 | nodemap.update(c.iri.getIRI, c.axiom) | ||
| 255 | val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) | ||
| 256 | if (unsafe contains e.getProperty) | ||
| 257 | (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) | ||
| 258 | else | ||
| 259 | (RSA.PE(term, c.iri) :: res, ext) | ||
| 260 | } | ||
| 261 | |||
| 262 | case _ => super.convert(expr, term, unsafe, skolem, suffix) | ||
| 263 | } | ||
| 264 | } | ||
| 265 | |||
| 266 | /* Ontology convertion into LP rules */ | ||
| 267 | val term = RSAOntology.genFreshVariable() | ||
| 268 | val result = axioms.map(a => | ||
| 269 | RSAConverter.convert(a, term, unsafe, new Constant(a), Empty) | ||
| 270 | ) | ||
| 271 | |||
| 272 | val datalog = result.unzip | ||
| 273 | val facts = datalog._1.flatten | ||
| 274 | var rules = datalog._2.flatten | ||
| 275 | |||
| 276 | /* Open connection with RDFox */ | ||
| 277 | val (server, data) = RDFoxUtil.openConnection("rsa_dependency_graph") | ||
| 278 | |||
| 279 | /* Add additional built-in rules */ | ||
| 280 | val varX = Variable.create("X") | ||
| 281 | val varY = Variable.create("Y") | ||
| 282 | rules = Rule.create( | ||
| 283 | RSA.E(varX, varY), | ||
| 284 | RSA.PE(varX, varY), | ||
| 285 | RSA.U(varX), | ||
| 286 | RSA.U(varY) | ||
| 287 | ) :: rules | ||
| 288 | /* Load facts and rules from ontology */ | ||
| 289 | RDFoxUtil.addFacts(data, facts) | ||
| 290 | RDFoxUtil.addRules(data, rules) | ||
| 291 | /* Load data files */ | ||
| 292 | RDFoxUtil.addData(data, datafiles: _*) | ||
| 293 | |||
| 294 | /* Build the graph */ | ||
| 295 | val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" | ||
| 296 | val answers = RDFoxUtil.submitQuery(data, query, RSA.Prefixes).get | ||
| 297 | var edges: Seq[DiEdge[Resource]] = | ||
| 298 | answers.collect { case (_, Seq(n1, n2)) => n1 ~> n2 } | ||
| 299 | val graph = Graph(edges: _*) | ||
| 300 | |||
| 301 | /* Close connection to RDFox */ | ||
| 302 | RDFoxUtil.closeConnection(server, data) | ||
| 303 | |||
| 304 | (graph, nodemap) | ||
| 305 | } | ||
| 306 | |||
| 307 | /** Approximate a Horn-ALCHOIQ ontology to RSA | 326 | /** Approximate a Horn-ALCHOIQ ontology to RSA |
| 308 | * | 327 | * |
| 309 | * This is done by gathering those axioms that prevent the ontology | 328 | * This is done by gathering those axioms that prevent the ontology |
| @@ -653,7 +672,7 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) { | |||
| 653 | val conflR = this.confl(roleR) | 672 | val conflR = this.confl(roleR) |
| 654 | // We just need the TBox to find | 673 | // We just need the TBox to find |
| 655 | val terms = for { | 674 | val terms = for { |
| 656 | axiom1 <- tbox | 675 | axiom1 <- axioms |
| 657 | if axiom1.isT5 | 676 | if axiom1.isT5 |
| 658 | // We expect only one role coming out of a T5 axiom | 677 | // We expect only one role coming out of a T5 axiom |
| 659 | roleS <- axiom1.objectPropertyExpressionsInSignature | 678 | roleS <- axiom1.objectPropertyExpressionsInSignature |
| @@ -693,7 +712,7 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) { | |||
| 693 | ) | 712 | ) |
| 694 | Logger.print( | 713 | Logger.print( |
| 695 | s"Logical axioms in Horn-ALCHOIQ ontology: ${ontology | 714 | s"Logical axioms in Horn-ALCHOIQ ontology: ${ontology |
| 696 | .getLogicalAxiomCount(true)} (${tbox.length}/${rbox.length}/${abox.length})", | 715 | .getLogicalAxiomCount(true)} (${axioms.length}/${axioms.length}/${axioms.length})", |
| 697 | level | 716 | level |
| 698 | ) | 717 | ) |
| 699 | Logger.print( | 718 | Logger.print( |
diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala new file mode 100644 index 0000000..073d0d9 --- /dev/null +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | package uk.ac.ox.cs.rsacomb.approximation | ||
| 2 | |||
| 3 | import java.io.File | ||
| 4 | import org.semanticweb.owlapi.model.OWLAxiom | ||
| 5 | |||
| 6 | /** Ontology approximation technique. */ | ||
| 7 | trait Approximation { | ||
| 8 | |||
| 9 | /** Approximate an ontology. | ||
| 10 | * | ||
| 11 | * @param ontology input ontology | ||
| 12 | * @return a new approximated ontology | ||
| 13 | */ | ||
| 14 | def approximate(ontology: Seq[OWLAxiom], datafiles: Seq[File]): Seq[OWLAxiom] | ||
| 15 | |||
| 16 | } | ||
diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala new file mode 100644 index 0000000..8036250 --- /dev/null +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala | |||
| @@ -0,0 +1,207 @@ | |||
| 1 | package uk.ac.ox.cs.rsacomb.approximation | ||
| 2 | |||
| 3 | /** Approximation algorithm that mantains soundness for CQ answering. | ||
| 4 | * | ||
| 5 | * The input OWL 2 ontology is assumed to be normalized and the output | ||
| 6 | * ontology is guaranteed to be in RSA. | ||
| 7 | * | ||
| 8 | * The algorithm is performed in three steps: | ||
| 9 | * 1. the ontology is reduced to ALCHOIQ by discarding any axiom | ||
| 10 | * that is not in the language; | ||
| 11 | * 2. the ontology is further reduced to Horn-ALCHOIQ by shifting | ||
| 12 | * axioms with disjunction on the rhs; | ||
| 13 | * 3. the ontology is approximated to RSA by manipulating its | ||
| 14 | * dependency graph. | ||
| 15 | * | ||
| 16 | * @see [[uk.ac.ox.cs.rsacomb.converter.Normalizer]] | ||
| 17 | */ | ||
| 18 | class LowerBound extends Approximation { | ||
| 19 | |||
| 20 | val normalizer = new Normalizer() | ||
| 21 | |||
| 22 | /** Main entry point for the approximation algorithm */ | ||
| 23 | def approximate( | ||
| 24 | ontology: Seq[OWLAxiom], | ||
| 25 | datafiles: Seq[File] | ||
| 26 | ): Seq[OWLAxiom] = { | ||
| 27 | /* Normalize axioms */ | ||
| 28 | val axioms1 = axioms flatMap normalizer.normalize | ||
| 29 | /* Delete any axiom outside of ALCHOIQ */ | ||
| 30 | val axioms2 = axioms1 filterNot inHornLACHOIQ | ||
| 31 | /* Shift any axiom with disjunction on the rhs */ | ||
| 32 | val axioms3 = for { | ||
| 33 | a1 <- axioms1 | ||
| 34 | a2 <- shift(a1) | ||
| 35 | a3 <- normalize(a2) | ||
| 36 | } yield a3 | ||
| 37 | /* Approximate to RSA */ | ||
| 38 | toRSA(axioms3, datafiles) | ||
| 39 | } | ||
| 40 | |||
| 41 | /** Discards all axioms outside ALCHOIQ */ | ||
| 42 | def inALCHOIQ(axiom: OWLLogicalAxiom): Boolean = | ||
| 43 | axiom match { | ||
| 44 | case a: OWLSubClassOfAxiom => { | ||
| 45 | val sub = a.getSubClass.getNNF | ||
| 46 | val sup = a.getSuperClass.getNNF | ||
| 47 | (sub, sup) match { | ||
| 48 | case (sub: OWLObjectAllValuesFrom, _) => false | ||
| 49 | case (sub: OWLDataAllValuesFrom, _) => false | ||
| 50 | case (_, sup: OWLDataAllValuesFrom) => false | ||
| 51 | case (sub: OWLObjectMinCardinality, _) if sub.getCardinality >= 2 => | ||
| 52 | false | ||
| 53 | case (sub: OWLDataMinCardinality, _) if sub.getCardinality >= 2 => | ||
| 54 | false | ||
| 55 | case (_, sup: OWLObjectMinCardinality) if sup.getCardinality >= 2 => | ||
| 56 | false | ||
| 57 | case (_, sup: OWLDataMinCardinality) if sup.getCardinality >= 2 => | ||
| 58 | false | ||
| 59 | case (sub: OWLObjectMaxCardinality, _) => false | ||
| 60 | case (sub: OWLDataMaxCardinality, _) => false | ||
| 61 | case (_, sup: OWLObjectMaxCardinality) if sup.getCardinality >= 2 => | ||
| 62 | false | ||
| 63 | case (_, sup: OWLDataMaxCardinality) if sup.getCardinality >= 1 => | ||
| 64 | false | ||
| 65 | case (_, sup: OWLObjectOneOf) if sup.getIndividuals.length > 2 => | ||
| 66 | false | ||
| 67 | case (sub: OWLObjectHasSelf, _) => false | ||
| 68 | case (_, sup: OWLObjectHasSelf) => false | ||
| 69 | case _ => true | ||
| 70 | } | ||
| 71 | } | ||
| 72 | case a: OWLTransitiveObjectPropertyAxiom => false | ||
| 73 | case a: OWLReflexiveObjectPropertyAxiom => false | ||
| 74 | case a: OWLSubPropertyChainOfAxiom => false | ||
| 75 | case a: OWLAsymmetricObjectPropertyAxiom => false | ||
| 76 | case a => true | ||
| 77 | } | ||
| 78 | |||
| 79 | /** Shifting axioms with disjunction on the rhs. | ||
| 80 | * | ||
| 81 | * The process of shifting presenves soundness but completenes w.r.t. | ||
| 82 | * CQ answering is lost. | ||
| 83 | * | ||
| 84 | * @example | ||
| 85 | * | ||
| 86 | * A -> B1 u B2 u B3 . | ||
| 87 | * | ||
| 88 | * becomes | ||
| 89 | * | ||
| 90 | * A n nB1 n nB2 -> B3 . | ||
| 91 | * A n nB1 n nB3 -> B2 . | ||
| 92 | * A n nB2 n nB3 -> B1 . | ||
| 93 | * nB1 n nB2 n nB3 -> nA . | ||
| 94 | * | ||
| 95 | * where nA, nB1, nB2, nB3 are fresh predicates "corresponding" to | ||
| 96 | * the negation of A, B1, B2, B3 respectively. | ||
| 97 | */ | ||
| 98 | def shift(axiom: OWLLogicalAxiom): Seq[OWLLogicalAxiom] = | ||
| 99 | axiom match { | ||
| 100 | case a: OWLSubClassOfAxiom => { | ||
| 101 | val sub = a.getSubClass.getNNF | ||
| 102 | val sup = a.getSuperClass.getNNF | ||
| 103 | sup match { | ||
| 104 | case sup: OWLObjectUnionOf => { | ||
| 105 | val body = sub.asConjunctSet.map((atom) => (atom, freshOWLClass())) | ||
| 106 | val head = sup.asDisjunctSet.map((atom) => (atom, freshOWLClass())) | ||
| 107 | |||
| 108 | val r1 = | ||
| 109 | factory.getOWLSubClassOfAxiom( | ||
| 110 | factory.getOWLObjectIntersectionOf( | ||
| 111 | (body.map(_._1) ++ head.map(_._2)): _* | ||
| 112 | ), | ||
| 113 | factory.getOWLNothing | ||
| 114 | ) | ||
| 115 | |||
| 116 | val r2s = | ||
| 117 | for { | ||
| 118 | (a, na) <- head | ||
| 119 | hs = head.map(_._2).filterNot(_ equals na) | ||
| 120 | } yield factory.getOWLSubClassOfAxiom( | ||
| 121 | factory.getOWLObjectIntersectionOf( | ||
| 122 | (body.map(_._1) ++ hs): _* | ||
| 123 | ), | ||
| 124 | a | ||
| 125 | ) | ||
| 126 | |||
| 127 | val r3s = | ||
| 128 | for { | ||
| 129 | (a, na) <- body | ||
| 130 | bs = body.map(_._1).filterNot(_ equals a) | ||
| 131 | } yield factory.getOWLSubClassOfAxiom( | ||
| 132 | factory.getOWLObjectIntersectionOf( | ||
| 133 | (bs ++ head.map(_._2)): _* | ||
| 134 | ), | ||
| 135 | na | ||
| 136 | ) | ||
| 137 | |||
| 138 | Seq(r1) ++ r2s ++ r3s | ||
| 139 | } | ||
| 140 | case _ => Seq(axiom) | ||
| 141 | } | ||
| 142 | } | ||
| 143 | case _ => Seq(axiom) | ||
| 144 | } | ||
| 145 | |||
| 146 | /** Approximate a Horn-ALCHOIQ ontology to RSA | ||
| 147 | * | ||
| 148 | * This is done by gathering those axioms that prevent the ontology | ||
| 149 | * dependency graph from being tree-shaped, and removing them. | ||
| 150 | * | ||
| 151 | * @param axioms the set of axioms to approximate. | ||
| 152 | * @return the approximated set of axioms. | ||
| 153 | */ | ||
| 154 | def toRSA(axioms: Seq[OWLAxiom], datafiles: Seq[File]): Seq[OWLAxiom] = { | ||
| 155 | /* Compute the dependency graph for the ontology */ | ||
| 156 | val (graph, nodemap) = RSAUtil.dependencyGraph(axioms, datafiles) | ||
| 157 | |||
| 158 | /* Define node colors for the graph visit */ | ||
| 159 | sealed trait NodeColor | ||
| 160 | case object Unvisited extends NodeColor | ||
| 161 | case object Visited extends NodeColor | ||
| 162 | case object ToDelete extends NodeColor | ||
| 163 | |||
| 164 | /* Keep track of node colors during graph visit */ | ||
| 165 | var color = Map.from[Resource, NodeColor]( | ||
| 166 | graph.nodes.toOuter.map(k => (k, Unvisited)) | ||
| 167 | ) | ||
| 168 | |||
| 169 | for { | ||
| 170 | component <- graph.componentTraverser().map(_ to Graph) | ||
| 171 | edge <- component | ||
| 172 | .outerEdgeTraverser(component.nodes.head) | ||
| 173 | .withKind(BreadthFirst) | ||
| 174 | } yield { | ||
| 175 | val source = edge._1 | ||
| 176 | val target = edge._2 | ||
| 177 | color(source) match { | ||
| 178 | case Unvisited | Visited => { | ||
| 179 | color(target) match { | ||
| 180 | case Unvisited => | ||
| 181 | color(source) = Visited; | ||
| 182 | color(target) = Visited | ||
| 183 | case Visited => | ||
| 184 | color(source) = ToDelete | ||
| 185 | case ToDelete => | ||
| 186 | color(source) = Visited | ||
| 187 | } | ||
| 188 | } | ||
| 189 | case ToDelete => | ||
| 190 | } | ||
| 191 | } | ||
| 192 | |||
| 193 | val toDelete = color.iterator.collect { case (resource: IRI, ToDelete) => | ||
| 194 | nodemap(resource.getIRI) | ||
| 195 | }.toSeq | ||
| 196 | |||
| 197 | /* Remove axioms from approximated ontology */ | ||
| 198 | axioms diff toDelete | ||
| 199 | } | ||
| 200 | // val edges1 = Seq('A ~> 'B, 'B ~> 'C, 'C ~> 'D, 'D ~> 'H, 'H ~> | ||
| 201 | // 'G, 'G ~> 'F, 'E ~> 'A, 'E ~> 'F, 'B ~> 'E, 'F ~> 'G, 'B ~> 'F, | ||
| 202 | // 'C ~> 'G, 'D ~> 'C, 'H ~> 'D) | ||
| 203 | // val edges2 = Seq('I ~> 'M, 'I ~> 'L, 'L ~> 'N, 'M ~> 'N) | ||
| 204 | // val edges3 = Seq('P ~> 'O) | ||
| 205 | // val graph = Graph.from(edges = edges1 ++ edges2 ++ edges3) | ||
| 206 | // | ||
| 207 | } | ||
