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 | } | ||