comparison service/mqtt_to_rdf/inference/inference_test.py @ 1727:23e6154e6c11

file moves
author drewp@bigasterisk.com
date Tue, 20 Jun 2023 23:26:24 -0700
parents service/mqtt_to_rdf/inference_test.py@73abfd4cf5d0
children
comparison
equal deleted inserted replaced
1726:7d3797ed6681 1727:23e6154e6c11
1 """
2 also see https://github.com/w3c/N3/tree/master/tests/N3Tests
3 """
4 import unittest
5 from decimal import Decimal
6 from pathlib import Path
7 from typing import cast
8
9 from rdflib import ConjunctiveGraph, Graph, Literal, Namespace
10 from rdflib.parser import StringInputSource
11
12 from inference.inference import Inference
13 from inference.rdflib_debug_patches import patchBnodeCounter, patchSlimReprs
14
15 patchSlimReprs()
16 patchBnodeCounter()
17
18 EX = Namespace('http://example.com/')
19 ROOM = Namespace('http://projects.bigasterisk.com/room/')
20
21
22 def N3(txt: str):
23 g = ConjunctiveGraph()
24 prefix = """
25 @prefix : <http://projects.bigasterisk.com/room/> .
26 @prefix ex: <http://example.com/> .
27 @prefix room: <http://projects.bigasterisk.com/room/> .
28 @prefix math: <http://www.w3.org/2000/10/swap/math#> .
29 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
30 """
31 g.parse(StringInputSource((prefix + txt).encode('utf8')), format='n3')
32 return g
33
34
35 def makeInferenceWithRules(n3):
36 inf = Inference()
37 inf.setRules(N3(n3))
38 return inf
39
40
41 class WithGraphEqual(unittest.TestCase):
42
43 def assertGraphEqual(self, g: Graph, expected: Graph):
44 stmts1 = list(g.triples((None, None, None)))
45 stmts2 = list(expected.triples((None, None, None)))
46 self.assertCountEqual(stmts1, stmts2)
47
48
49 class TestInferenceWithoutVars(WithGraphEqual):
50
51 def testEmitNothing(self):
52 inf = makeInferenceWithRules("")
53 implied = inf.infer(N3(":a :b :c ."))
54 self.assertEqual(len(implied), 0)
55
56 def testSimple(self):
57 inf = makeInferenceWithRules("{ :a :b :c . } => { :a :b :new . } .")
58 implied = inf.infer(N3(":a :b :c ."))
59 self.assertGraphEqual(implied, N3(":a :b :new ."))
60
61 def testTwoRounds(self):
62 inf = makeInferenceWithRules("""
63 { :a :b :c . } => { :a :b :new1 . } .
64 { :a :b :new1 . } => { :a :b :new2 . } .
65 """)
66
67 implied = inf.infer(N3(":a :b :c ."))
68 self.assertGraphEqual(implied, N3(":a :b :new1, :new2 ."))
69
70
71 class TestNonRuleStatements(WithGraphEqual):
72
73 def test(self):
74 inf = makeInferenceWithRules(":d :e :f . { :a :b :c . } => { :a :b :new . } .")
75 self.assertCountEqual(inf.nonRuleStatements(), [(ROOM.d, ROOM.e, ROOM.f)])
76
77
78 class TestInferenceWithVars(WithGraphEqual):
79
80 def testVarInSubject(self):
81 inf = makeInferenceWithRules("{ ?x :b :c . } => { :new :stmt ?x } .")
82 implied = inf.infer(N3(":a :b :c ."))
83 self.assertGraphEqual(implied, N3(":new :stmt :a ."))
84
85 def testVarInObject(self):
86 inf = makeInferenceWithRules("{ :a :b ?x . } => { :new :stmt ?x } .")
87 implied = inf.infer(N3(":a :b :c ."))
88 self.assertGraphEqual(implied, N3(":new :stmt :c ."))
89
90 def testVarMatchesTwice(self):
91 inf = makeInferenceWithRules("{ :a :b ?x . } => { :new :stmt ?x } .")
92 implied = inf.infer(N3(":a :b :c, :d ."))
93 self.assertGraphEqual(implied, N3(":new :stmt :c, :d ."))
94
95 def testTwoRulesApplyIndependently(self):
96 inf = makeInferenceWithRules("""
97 { :a :b ?x . } => { :new :stmt ?x . } .
98 { :d :e ?y . } => { :new :stmt2 ?y . } .
99 """)
100 implied = inf.infer(N3(":a :b :c ."))
101 self.assertGraphEqual(implied, N3("""
102 :new :stmt :c .
103 """))
104 implied = inf.infer(N3(":a :b :c . :d :e :f ."))
105 self.assertGraphEqual(implied, N3("""
106 :new :stmt :c .
107 :new :stmt2 :f .
108 """))
109
110 def testOneRuleActivatesAnother(self):
111 inf = makeInferenceWithRules("""
112 { :a :b ?x . } => { :new :stmt ?x . } .
113 { ?y :stmt ?z . } => { :new :stmt2 ?y . } .
114 """)
115 implied = inf.infer(N3(":a :b :c ."))
116 self.assertGraphEqual(implied, N3("""
117 :new :stmt :c .
118 :new :stmt2 :new .
119 """))
120
121 def testRuleMatchesStaticStatement(self):
122 inf = makeInferenceWithRules("{ :a :b ?x . :a :b :c . } => { :new :stmt ?x } .")
123 implied = inf.infer(N3(":a :b :c ."))
124 self.assertGraphEqual(implied, N3(":new :stmt :c ."))
125
126
127 class TestVarLinksTwoStatements(WithGraphEqual):
128
129 def setUp(self):
130 self.inf = makeInferenceWithRules("{ :a :b ?x . :d :e ?x } => { :new :stmt ?x } .")
131
132 def testOnlyOneStatementPresent(self):
133 implied = self.inf.infer(N3(":a :b :c ."))
134 self.assertGraphEqual(implied, N3(""))
135
136 def testObjectsConflict(self):
137 implied = self.inf.infer(N3(":a :b :c . :d :e :f ."))
138 self.assertGraphEqual(implied, N3(""))
139
140 def testObjectsAgree(self):
141 implied = self.inf.infer(N3(":a :b :c . :d :e :c ."))
142 self.assertGraphEqual(implied, N3(":new :stmt :c ."))
143
144
145 class TestBnodeMatching(WithGraphEqual):
146
147 def testRuleBnodeBindsToInputBnode(self):
148 inf = makeInferenceWithRules("{ [ :a :b ] . } => { :new :stmt :here } .")
149 implied = inf.infer(N3("[ :a :b ] ."))
150 self.assertGraphEqual(implied, N3(":new :stmt :here ."))
151
152 def testRuleVarBindsToInputBNode(self):
153 inf = makeInferenceWithRules("{ ?z :a :b . } => { :new :stmt :here } .")
154 implied = inf.infer(N3("[] :a :b ."))
155 self.assertGraphEqual(implied, N3(":new :stmt :here ."))
156
157
158 class TestBnodeAliasingSetup(WithGraphEqual):
159
160 def setUp(self):
161 self.inf = makeInferenceWithRules("""
162 {
163 ?var0 :a ?x; :b ?y .
164 } => {
165 :xVar :value ?x .
166 :yVar :value ?y .
167 } .
168 """)
169
170 def assertResult(self, actual):
171 self.assertGraphEqual(actual, N3("""
172 :xVar :value :x0, :x1 .
173 :yVar :value :y0, :y1 .
174 """))
175
176 def testMatchesDistinctStatements(self):
177 implied = self.inf.infer(N3("""
178 :stmt0 :a :x0; :b :y0 .
179 :stmt1 :a :x1; :b :y1 .
180 """))
181 self.assertResult(implied)
182
183 def testMatchesDistinctBnodes(self):
184 implied = self.inf.infer(N3("""
185 [ :a :x0; :b :y0 ] .
186 [ :a :x1; :b :y1 ] .
187 """))
188 self.assertResult(implied)
189
190 def testProdCase(self):
191 inf = makeInferenceWithRules('''
192 {
193 :AirQualitySensor :nameRemap [
194 :sensorName ?sensorName;
195 :measurementName ?measurement
196 ] .
197 } => {
198 :a :b ?sensorName.
199 :d :e ?measurement.
200 } .
201 ''')
202 implied = inf.infer(
203 N3('''
204 :AirQualitySensor :nameRemap
205 [:sensorName "bme280_pressure"; :measurementName "pressure"],
206 [:sensorName "bme280_temperature"; :measurementName "temperature"] .
207 '''))
208
209 self.assertGraphEqual(implied, N3('''
210 :a :b "bme280_pressure", "bme280_temperature" .
211 :d :e "pressure", "temperature" .
212 '''))
213
214
215 class TestBnodeGenerating(WithGraphEqual):
216
217 def testRuleBnodeMakesNewBnode(self):
218 inf = makeInferenceWithRules("{ [ :a :b ] . } => { [ :c :d ] } .")
219 implied = inf.infer(N3("[ :a :b ] ."))
220 ruleNode = list(inf.rules[0].rhsGraph)[0]
221 stmt0Node = list(implied)[0][0]
222 self.assertNotEqual(ruleNode, stmt0Node)
223
224 def testRuleBnodeMakesNewBnodesEachTime(self):
225 inf = makeInferenceWithRules("{ [ :a ?x ] . } => { [ :c :d ] } .")
226 implied = inf.infer(N3("[ :a :b, :e ] ."))
227 ruleNode = list(inf.rules[0].rhsGraph)[0]
228 stmt0Node = list(implied)[0][0]
229 stmt1Node = list(implied)[1][0]
230
231 self.assertNotEqual(ruleNode, stmt0Node)
232 self.assertNotEqual(ruleNode, stmt1Node)
233 self.assertNotEqual(stmt0Node, stmt1Node)
234
235
236 class TestSelfFulfillingRule(WithGraphEqual):
237
238 def test1(self):
239 inf = makeInferenceWithRules("{ } => { :new :stmt :x } .")
240 self.assertGraphEqual(inf.infer(N3("")), N3(":new :stmt :x ."))
241 self.assertGraphEqual(inf.infer(N3(":any :any :any .")), N3(":new :stmt :x ."))
242
243 # def test2(self):
244 # inf = makeInferenceWithRules("{ (2) math:sum ?x } => { :new :stmt ?x } .")
245 # self.assertGraphEqual(inf.infer(N3("")), N3(":new :stmt 2 ."))
246
247 # @unittest.skip("too hard for now")
248 # def test3(self):
249 # inf = makeInferenceWithRules("{ :a :b :c . :a :b ?x . } => { :new :stmt ?x } .")
250 # self.assertGraphEqual(inf.infer(N3("")), N3(":new :stmt :c ."))
251
252
253 class TestInferenceWithMathFunctions(WithGraphEqual):
254
255 def testBoolFilter(self):
256 inf = makeInferenceWithRules("{ :a :b ?x . ?x math:greaterThan 5 } => { :new :stmt ?x } .")
257 self.assertGraphEqual(inf.infer(N3(":a :b 3 .")), N3(""))
258 self.assertGraphEqual(inf.infer(N3(":a :b 5 .")), N3(""))
259 self.assertGraphEqual(inf.infer(N3(":a :b 6 .")), N3(":new :stmt 6 ."))
260
261 def testNonFiringMathRule(self):
262 inf = makeInferenceWithRules("{ :a :b ?x . (?x 1) math:sum ?y } => { :new :stmt ?y } .")
263 self.assertGraphEqual(inf.infer(N3("")), N3(""))
264
265 def testStatementGeneratingRule(self):
266 inf = makeInferenceWithRules("{ :a :b ?x . (?x) math:sum ?y } => { :new :stmt ?y } .")
267 self.assertGraphEqual(inf.infer(N3(":a :b 3 .")), N3(":new :stmt 3 ."))
268
269 def test2Operands(self):
270 inf = makeInferenceWithRules("{ :a :b ?x . (?x 1) math:sum ?y } => { :new :stmt ?y } .")
271 self.assertGraphEqual(inf.infer(N3(":a :b 3 .")), N3(":new :stmt 4 ."))
272
273 def test3Operands(self):
274 inf = makeInferenceWithRules("{ :a :b ?x . (2 ?x 2) math:sum ?y } => { :new :stmt ?y } .")
275 self.assertGraphEqual(inf.infer(N3(":a :b 2 .")), N3(":new :stmt 6 ."))
276
277 # def test0Operands(self):
278 # inf = makeInferenceWithRules("{ :a :b ?x . () math:sum ?y } => { :new :stmt ?y } .")
279 # self.assertGraphEqual(inf.infer(N3(":a :b 2 .")), N3(":new :stmt 0 ."))
280
281
282 class TestInferenceWithCustomFunctions(WithGraphEqual):
283
284 def testAsFarenheit(self):
285 inf = makeInferenceWithRules("{ :a :b ?x . ?x room:asFarenheit ?f } => { :new :stmt ?f } .")
286 self.assertGraphEqual(inf.infer(N3(":a :b 12 .")), N3(":new :stmt 53.6 ."))
287
288 def testChildResource(self):
289 inf = makeInferenceWithRules("{ :a :b ?x . (:c ?x) room:childResource ?y .} => { :new :stmt ?y } .")
290 self.assertGraphEqual(inf.infer(N3(':a :b "foo" .')), N3(":new :stmt <http://projects.bigasterisk.com/room/c/foo> ."))
291
292 def testChildResourceSegmentQuoting(self):
293 inf = makeInferenceWithRules("{ :a :b ?x . (:c ?x) room:childResource ?y .} => { :new :stmt ?y } .")
294 self.assertGraphEqual(inf.infer(N3(':a :b "b / w -> #." .')), N3(":new :stmt <http://projects.bigasterisk.com/room/c/b%20%2F%20w%20-%3E%20%23.> ."))
295
296
297 class TestUseCases(WithGraphEqual):
298
299 def testSimpleTopic(self):
300 inf = makeInferenceWithRules('''
301 { ?msg :body "online" . } => { ?msg :onlineTerm :Online . } .
302 { ?msg :body "offline" . } => { ?msg :onlineTerm :Offline . } .
303
304 {
305 ?msg a :MqttMessage ;
306 :topic :foo;
307 :onlineTerm ?onlineness . } => {
308 :frontDoorLockStatus :connectedStatus ?onlineness .
309 } .
310 ''')
311
312 out = inf.infer(N3('[] a :MqttMessage ; :body "online" ; :topic :foo .'))
313 self.assertIn((ROOM['frontDoorLockStatus'], ROOM['connectedStatus'], ROOM['Online']), out)
314
315 def testTopicIsList(self):
316 inf = makeInferenceWithRules('''
317 { ?msg :body "online" . } => { ?msg :onlineTerm :Online . } .
318 { ?msg :body "offline" . } => { ?msg :onlineTerm :Offline . } .
319
320 {
321 ?msg a :MqttMessage ;
322 :topic ( "frontdoorlock" "status" );
323 :onlineTerm ?onlineness . } => {
324 :frontDoorLockStatus :connectedStatus ?onlineness .
325 } .
326 ''')
327
328 out = inf.infer(N3('[] a :MqttMessage ; :body "online" ; :topic ( "frontdoorlock" "status" ) .'))
329 self.assertIn((ROOM['frontDoorLockStatus'], ROOM['connectedStatus'], ROOM['Online']), out)
330
331 def testPerformance0(self):
332 inf = makeInferenceWithRules('''
333 {
334 ?msg a :MqttMessage;
335 :topic :topic1;
336 :bodyFloat ?valueC .
337 ?valueC math:greaterThan -999 .
338 ?valueC room:asFarenheit ?valueF .
339 } => {
340 :airQualityIndoorTemperature :temperatureF ?valueF .
341 } .
342 ''')
343 out = inf.infer(
344 N3('''
345 <urn:uuid:c6e1d92c-0ee1-11ec-bdbd-2a42c4691e9a> a :MqttMessage ;
346 :body "23.9" ;
347 :bodyFloat 2.39e+01 ;
348 :topic :topic1 .
349 '''))
350
351 vlit = cast(Literal, out.value(ROOM['airQualityIndoorTemperature'], ROOM['temperatureF']))
352 valueF = cast(Decimal, vlit.toPython())
353 self.assertAlmostEqual(float(valueF), 75.02)
354
355 def testPerformance1(self):
356 inf = makeInferenceWithRules('''
357 {
358 ?msg a :MqttMessage;
359 :topic ( "air_quality_indoor" "sensor" "bme280_temperature" "state" );
360 :bodyFloat ?valueC .
361 ?valueC math:greaterThan -999 .
362 ?valueC room:asFarenheit ?valueF .
363 } => {
364 :airQualityIndoorTemperature :temperatureF ?valueF .
365 } .
366 ''')
367 out = inf.infer(
368 N3('''
369 <urn:uuid:c6e1d92c-0ee1-11ec-bdbd-2a42c4691e9a> a :MqttMessage ;
370 :body "23.9" ;
371 :bodyFloat 2.39e+01 ;
372 :topic ( "air_quality_indoor" "sensor" "bme280_temperature" "state" ) .
373 '''))
374 vlit = cast(Literal, out.value(ROOM['airQualityIndoorTemperature'], ROOM['temperatureF']))
375 valueF = cast(Decimal, vlit.toPython())
376 self.assertAlmostEqual(float(valueF), 75.02)
377
378 def testEmitBnodes(self):
379 inf = makeInferenceWithRules('''
380 { ?s a :AirQualitySensor; :label ?name . } => {
381 [ a :MqttStatementSource;
382 :mqttTopic (?name "sensor" "bme280_temperature" "state") ] .
383 } .
384 ''')
385 out = inf.infer(N3('''
386 :airQualityOutdoor a :AirQualitySensor; :label "air_quality_outdoor" .
387 '''))
388 out.bind('', ROOM)
389 out.bind('ex', EX)
390 self.assertEqual(
391 out.serialize(format='n3'), b'''\
392 @prefix : <http://projects.bigasterisk.com/room/> .
393 @prefix ex: <http://example.com/> .
394 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
395 @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
396 @prefix xml: <http://www.w3.org/XML/1998/namespace> .
397 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
398
399 [] a :MqttStatementSource ;
400 :mqttTopic ( "air_quality_outdoor" "sensor" "bme280_temperature" "state" ) .
401
402 ''')
403
404 def testRemap(self):
405 inf = makeInferenceWithRules('''
406 {
407 ?sensor a :AirQualitySensor; :label ?name .
408 (:mqttSource ?name) :childResource ?base .
409 } => {
410 ?sensor :statementSourceBase ?base .
411 } .
412 ''')
413 out = inf.infer(
414 N3('''
415 :airQualityIndoor a :AirQualitySensor; :label "air_quality_indoor" .
416 :airQualityOutdoor a :AirQualitySensor; :label "air_quality_outdoor" .
417 '''), Path('/tmp/log.html'))
418 self.assertGraphEqual(
419 out,
420 N3('''
421 :airQualityIndoor :statementSourceBase <http://projects.bigasterisk.com/room/mqttSource/air_quality_indoor> .
422 :airQualityOutdoor :statementSourceBase <http://projects.bigasterisk.com/room/mqttSource/air_quality_outdoor> .
423 '''))
424
425
426 class TestListPerformance(WithGraphEqual):
427
428 def testList1(self):
429 inf = makeInferenceWithRules("{ :a :b (:e0) . } => { :new :stmt :here } .")
430 implied = inf.infer(N3(":a :b (:e0) ."))
431 self.assertGraphEqual(implied, N3(":new :stmt :here ."))
432
433 def testList2(self):
434 inf = makeInferenceWithRules("{ :a :b (:e0 :e1) . } => { :new :stmt :here } .")
435 implied = inf.infer(N3(":a :b (:e0 :e1) ."))
436 self.assertGraphEqual(implied, N3(":new :stmt :here ."))
437
438 def testList3(self):
439 inf = makeInferenceWithRules("{ :a :b (:e0 :e1 :e2) . } => { :new :stmt :here } .")
440 implied = inf.infer(N3(":a :b (:e0 :e1 :e2) ."))
441 self.assertGraphEqual(implied, N3(":new :stmt :here ."))
442
443 # def testList4(self):
444 # inf = makeInferenceWithRules("{ :a :b (:e0 :e1 :e2 :e3) . } => { :new :stmt :here } .")
445 # implied = inf.infer(N3(":a :b (:e0 :e1 :e2 :e3) ."))
446 # self.assertGraphEqual(implied, N3(":new :stmt :here ."))