From d9e4cd4c6f91f46dd9da6a0ded783506c4167fed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9verin=20Lemaignan?= Date: Fri, 5 Dec 2014 14:56:22 +0100 Subject: [PATCH] [tests] Improved the ontologic reasonner The get_missig_taxonomy_stmts method was not complete, a lot of possible deductions was neglected because 1) dynamic onto classes was not updated inside recursives iterations 2) it didn't return new equivalent classes Added tests (test_reason.py) to bring out the importance of this improvement --- .../services/simple_rdfs_reasoner.py | 150 +++++++++++++----- testing/test.py | 54 ------- testing/test_reasoner.py | 99 +++++++++--- 3 files changed, 187 insertions(+), 116 deletions(-) diff --git a/src/minimalkb/services/simple_rdfs_reasoner.py b/src/minimalkb/services/simple_rdfs_reasoner.py index a4a119b..488d52d 100644 --- a/src/minimalkb/services/simple_rdfs_reasoner.py +++ b/src/minimalkb/services/simple_rdfs_reasoner.py @@ -23,6 +23,110 @@ def __repr__(self): "\n\tParents: " + str(self.parents) + \ "\n\tChildren: " + str(self.children) + \ "\n\tInstances: " + str(self.instances) + + + +# recursive functions for ontologic inheritances : +#------------------------------------------------ + +def addequivalent(equivalentclasses, cls, equivalent, memory=None): + ''' + propagation of equivalence properties : + --------------------------------------- + 1) transitivity : (A = B) and (B = C) then (A = C) + 2) symetry : (A = B) then (B = A) + 3) the reflexive property results from symetry and transitivity. + + we need to update the classes to make the additions available for the other calls of inheritance functions + + the memory set is used to prevent the method to loop infinitly + for instance : + + let B in the set of equivalents of A + then the method do with (A and B) : + + 1) equivalentclasses.add(A.name , B.name) + 2) A is added to the set of equivalents of B + 3) for every equivalents C of the set of B do the same with (A and C) + but inside this set we find A (we just added it !) + so : + 1) equivalentclasses.add(A.name , A.name) --> ok, reflexivity is added + 2) B is added to the set of equivalents of A --> (no effect because it was already here, but ok) + 3) for every equivalents C of the set of A do the same with (A and C) + but inside we find B !!! + so : + without memory, it loops infinitly + but with memory, it finds that B have already been handled + ''' + if not memory: + memory = set() + + if equivalent not in memory: + memory.add(equivalent) + + equivalentclasses.add((cls.name, equivalent.name)) + cls.equivalents.add(equivalent) # update classes + + # reflexive property : + equivalent.equivalents.add(cls) # update classes + + # transitive property : + for e in frozenset(equivalent.equivalents): + addequivalent(equivalentclasses, cls, e, memory) + + +def addsubclassof(subclassof, scls, cls): + ''' + propagation of inclusions : + --------------------------- + the inclusions are just transitives : (A in B) and (B in C) then (A in C) + + we need to update the classes to make the additions available for the other calls of inheritance functions + ''' + subclassof.add((scls.name, cls.name)) + # update classes + cls.children.add(scls) + scls.parents.add(cls) + + # transitivity : + for p in frozenset(cls.parents): + addsubclassof(subclassof, scls, p) + +def addoverclassof(subclassof, cls, ocls): + ''' + back-track propagation of inclusion : + ------------------------------------- + this backtracking seems to be unusful (it does the same thing than in addsubclassof) + but is used after adding equivalences : + indeed, the property " (A = B) and (C in A) then (C in B) " cannot be taken in account by the method addsubclassof + + we need to update the classes to make the additions available for the other calls of inheritance functions + ''' + + subclassof.add((cls.name, ocls.name)) + # update classes : + ocls.children.add(cls) + cls.parents.add(ocls) + + # transitivity : + for c in frozenset(cls.children): + addoverclassof(subclassof, c, cls) + +def addinstance(rdftype, instance, cls): + ''' + propagation of instances : + --------------------------- + the instances are just transitives : (A in B) and (B in C) then (A in C) + + don't need to update the classes because they are not used by the other functions + ''' + + rdftype.add((instance, cls.name)) + + for p in cls.parents: + addinstance(rdftype, instance, p) + +#---------------------------------------- class SQLiteSimpleRDFSReasoner: @@ -121,6 +225,8 @@ def get_onto(self, db, model = DEFAULT_MODEL): return onto, rdftype, subclassof, equivalentclasses + + def get_missing_taxonomy_stmts(self, model = DEFAULT_MODEL): @@ -129,52 +235,24 @@ def get_missing_taxonomy_stmts(self, model = DEFAULT_MODEL): newrdftype = set() newsubclassof = set() newequivalentclasses=set() - - - def addinstance(instance, cls): - newrdftype.add((instance, cls.name)) - for p in cls.parents: - addinstance(instance, p) - - def addoverclassof(cls, ocls): - newsubclassof.add((cls.name, ocls.name)) - ocls.children.add(cls) - cls.parents.add(ocls) - for c in frozenset(cls.children): - addoverclassof(c, cls) - - def addsubclassof(scls, cls): - newsubclassof.add((scls.name, cls.name)) - cls.children.add(scls) - scls.parents.add(cls) - for p in frozenset(cls.parents): - addsubclassof(scls, p) - - def addequivalent(cls, equ, memory): - if equ not in memory: - memory.add(equ) - newequivalentclasses.add((cls.name, equ.name)) - cls.equivalents.add(equ) - equ.equivalents.add(cls) - for e in frozenset(equ.equivalents): - addequivalent(cls, e, memory) + for name, cls in onto.items(): for p in frozenset(cls.parents): - addsubclassof(cls, p) + addsubclassof(newsubclassof, cls, p) for i in cls.instances: - addinstance(i, cls) + addinstance(newrdftype, i, cls) - memory = set() for equivalent in frozenset(cls.equivalents): - addequivalent(cls, equivalent, memory) + addequivalent(newequivalentclasses, cls, equivalent) + for equivalent in cls.equivalents: for p in frozenset(cls.parents): - addsubclassof(equivalent, p) + addsubclassof(newsubclassof, equivalent, p) for c in frozenset(cls.children): - addoverclassof(c, equivalent) + addoverclassof(newsubclassof, c, equivalent) for i in cls.instances: - addinstance(i, equivalent) + addinstance(newrdftype, i, equivalent) diff --git a/testing/test.py b/testing/test.py index ef2c6d6..8e20dd9 100755 --- a/testing/test.py +++ b/testing/test.py @@ -87,17 +87,6 @@ def test_existence(self): self.assertFalse('alfred likes icecream' in self.kb) self.assertFalse('alfred' in self.kb) - def test_existence_with_inference(self): - """ Requires a RDFS reasoner to run. - """ - self.kb += ["alfred rdf:type Human", "Human rdfs:subClassOf Animal"] - time.sleep(REASONING_DELAY) - self.assertTrue('alfred rdf:type Animal' in self.kb) - - self.kb += ["Animal rdfs:subClassOf Thing"] - time.sleep(REASONING_DELAY) - self.assertTrue('alfred rdf:type Thing' in self.kb) - def test_lookup(self): self.assertItemsEqual(self.kb.lookup('alfred'), []) @@ -380,36 +369,6 @@ def test_complex_events(self): self.assertEqual(id, evtid) self.assertItemsEqual(value, [u"alfred"]) - def test_complex_events_rdfs(self): - """ Requires a RDFS reasoner to run. - """ - evtid = self.kb.subscribe(["?a desires ?act", "?act rdf:type Action"], var = "a") - - # should not trigger an event - self.kb += ["alfred desires ragnagna"] - time.sleep(0.1) - - with self.assertRaises(Empty): - self.kb.events.get_nowait() - - # should not trigger an event - self.kb += ["ragnagna rdf:type Zorro"] - time.sleep(0.1) - - with self.assertRaises(Empty): - self.kb.events.get_nowait() - - # should trigger an event - self.kb += ["Zorro rdfs:subClassOf Action"] - time.sleep(REASONING_DELAY) - # required to ensure the event is triggered after classification! - self.kb += ["nop nop nop"] - time.sleep(0.1) - - id, value = self.kb.events.get_nowait() - self.assertEqual(id, evtid) - self.assertItemsEqual(value, [u"alfred"]) - def test_taxonomy_walking(self): self.assertFalse(self.kb.classesof("john")) @@ -420,19 +379,6 @@ def test_taxonomy_walking(self): self.kb -= ["john rdf:type Human"] self.assertItemsEqual(self.kb.classesof("john"), [u'Genius']) - def test_taxonomy_walking_inheritance(self): - """ Requires a RDFS reasoner to run. - """ - self.kb += ["john rdf:type Human"] - self.assertItemsEqual(self.kb.classesof("john"), [u'Human']) - self.kb += ["Human rdfs:subClassOf Animal"] - time.sleep(REASONING_DELAY) - self.assertItemsEqual(self.kb.classesof("john"), [u'Human', u'Animal']) - self.assertItemsEqual(self.kb.classesof("john", True), [u'Human']) - self.kb -= ["john rdf:type Human"] - time.sleep(REASONING_DELAY) - self.assertFalse(self.kb.classesof("john")) - def test_memory(self): self.kb.add(["john rdf:type Human"]) diff --git a/testing/test_reasoner.py b/testing/test_reasoner.py index a1dbae8..782498f 100644 --- a/testing/test_reasoner.py +++ b/testing/test_reasoner.py @@ -12,46 +12,93 @@ REASONING_DELAY = 0.2 -class TestSequenceFunctions(unittest.TestCase): +class TestRDFSReasoner(unittest.TestCase): - def setUp(self): - self.kb = kb.KB() - self.kb.clear() + def setUp(self): + self.kb = kb.KB() + self.kb.clear() - def tearDown(self): - self.kb.close() + def tearDown(self): + self.kb.close() - def test_reason(self): + def test_complex_events_rdfs(self): - # basic adds : + evtid = self.kb.subscribe(["?a desires ?act", "?act rdf:type Action"], var = "a") - self.kb += ['self rdf:type robot'] - - self.kb.add(['robot rdfs:subClassOf agent', 'agent rdfs:subClassOf humanoide', 'human rdfs:subClassOf animals']) + # should not trigger an event + self.kb += ["alfred desires ragnagna"] + time.sleep(0.1) - self.kb.add(['robot owl:equivalentClass machine', 'machine owl:equivalentClass automate']) - - self.kb += ['rp2 rdfs:subClassOf automate'] - - # basic tests : + with self.assertRaises(Empty): + self.kb.events.get_nowait() - self.assertTrue('self rdf:type robot' in self.kb) + # should not trigger an event + self.kb += ["ragnagna rdf:type Zorro"] + time.sleep(0.1) - time.sleep(0.1) + with self.assertRaises(Empty): + self.kb.events.get_nowait() - self.assertTrue('robot rdfs:subClassOf humanoide' in self.kb) - self.assertTrue('self rdf:type humanoide' in self.kb) - self.assertTrue('robot owl:equivalentClass automate' in self.kb) - self.assertTrue('self rdf:type machine' in self.kb) - self.assertTrue('self rdf:type automate' in self.kb) + # should trigger an event + self.kb += ["Zorro rdfs:subClassOf Action"] + time.sleep(REASONING_DELAY) + # required to ensure the event is triggered after classification! + self.kb += ["nop nop nop"] + time.sleep(0.1) - self.assertTrue('rp2 rdfs:subClassOf robot' in self.kb) + id, value = self.kb.events.get_nowait() + self.assertEqual(id, evtid) + self.assertItemsEqual(value, [u"alfred"]) - + def test_taxonomy_walking_inheritance(self): + + self.kb += ["john rdf:type Human"] + self.assertItemsEqual(self.kb.classesof("john"), [u'Human']) + self.kb += ["Human rdfs:subClassOf Animal"] + time.sleep(REASONING_DELAY) + self.assertItemsEqual(self.kb.classesof("john"), [u'Human', u'Animal']) + self.assertItemsEqual(self.kb.classesof("john", True), [u'Human']) + self.kb -= ["john rdf:type Human"] + time.sleep(REASONING_DELAY) + self.assertFalse(self.kb.classesof("john")) + + + def test_second_level_inheritance(self): + + self.kb += 'myself rdf:type Robot' + self.kb += ['Robot rdfs:subClassOf Agent', 'Agent rdfs:subClassOf PhysicalEntity'] + + time.sleep(REASONING_DELAY) + + self.assertTrue('Robot rdfs:subClassOf PhysicalEntity' in self.kb) + self.assertTrue('myself rdf:type PhysicalEntity' in self.kb) + + + def test_equivalent_classes_transitivity(self): + + self.kb += 'myself rdf:type Robot' + self.kb += ['Robot owl:equivalentClass Machine', 'Machine owl:equivalentClass Automaton'] + self.kb += 'PR2 rdfs:subClassOf Automaton' + + time.sleep(REASONING_DELAY) + + self.assertTrue('Robot owl:equivalentClass Automaton' in self.kb) + self.assertItemsEqual(self.kb.classesof("myself"), [u'Robot', u'Machine', u'Automaton']) + self.assertTrue('PR2 rdfs:subClassOf Robot' in self.kb) + + def test_existence_with_inference(self): + + self.kb += ["alfred rdf:type Human", "Human rdfs:subClassOf Animal"] + time.sleep(REASONING_DELAY) + self.assertTrue('alfred rdf:type Animal' in self.kb) + + self.kb += ["Animal rdfs:subClassOf Thing"] + time.sleep(REASONING_DELAY) + self.assertTrue('alfred rdf:type Thing' in self.kb) def version(): - print("minimalKB tests %s" % __version__) + print("minimalKB RDFS reasoner tests %s" % __version__) if __name__ == '__main__':