Mercurial > code > home > repos > homeauto
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 .")) |