diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/mqtt_to_rdf/inference/inference_test.py	Tue Jun 20 23:26:24 2023 -0700
@@ -0,0 +1,446 @@
+"""
+also see https://github.com/w3c/N3/tree/master/tests/N3Tests
+"""
+import unittest
+from decimal import Decimal
+from pathlib import Path
+from typing import cast
+
+from rdflib import ConjunctiveGraph, Graph, Literal, Namespace
+from rdflib.parser import StringInputSource
+
+from inference.inference import Inference
+from inference.rdflib_debug_patches import patchBnodeCounter, patchSlimReprs
+
+patchSlimReprs()
+patchBnodeCounter()
+
+EX = Namespace('http://example.com/')
+ROOM = Namespace('http://projects.bigasterisk.com/room/')
+
+
+def N3(txt: str):
+    g = ConjunctiveGraph()
+    prefix = """
+@prefix : <http://projects.bigasterisk.com/room/> .
+@prefix ex: <http://example.com/> .
+@prefix room: <http://projects.bigasterisk.com/room/> .
+@prefix math: <http://www.w3.org/2000/10/swap/math#> .
+@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
+"""
+    g.parse(StringInputSource((prefix + txt).encode('utf8')), format='n3')
+    return g
+
+
+def makeInferenceWithRules(n3):
+    inf = Inference()
+    inf.setRules(N3(n3))
+    return inf
+
+
+class WithGraphEqual(unittest.TestCase):
+
+    def assertGraphEqual(self, g: Graph, expected: Graph):
+        stmts1 = list(g.triples((None, None, None)))
+        stmts2 = list(expected.triples((None, None, None)))
+        self.assertCountEqual(stmts1, stmts2)
+
+
+class TestInferenceWithoutVars(WithGraphEqual):
+
+    def testEmitNothing(self):
+        inf = makeInferenceWithRules("")
+        implied = inf.infer(N3(":a :b :c ."))
+        self.assertEqual(len(implied), 0)
+
+    def testSimple(self):
+        inf = makeInferenceWithRules("{ :a :b :c . } => { :a :b :new . } .")
+        implied = inf.infer(N3(":a :b :c ."))
+        self.assertGraphEqual(implied, N3(":a :b :new ."))
+
+    def testTwoRounds(self):
+        inf = makeInferenceWithRules("""
+        { :a :b :c . } => { :a :b :new1 . } .
+        { :a :b :new1 . } => { :a :b :new2 . } .
+        """)
+
+        implied = inf.infer(N3(":a :b :c ."))
+        self.assertGraphEqual(implied, N3(":a :b :new1, :new2 ."))
+
+
+class TestNonRuleStatements(WithGraphEqual):
+
+    def test(self):
+        inf = makeInferenceWithRules(":d :e :f . { :a :b :c . } => { :a :b :new . } .")
+        self.assertCountEqual(inf.nonRuleStatements(), [(ROOM.d, ROOM.e, ROOM.f)])
+
+
+class TestInferenceWithVars(WithGraphEqual):
+
+    def testVarInSubject(self):
+        inf = makeInferenceWithRules("{ ?x :b :c . } => { :new :stmt ?x } .")
+        implied = inf.infer(N3(":a :b :c ."))
+        self.assertGraphEqual(implied, N3(":new :stmt :a ."))
+
+    def testVarInObject(self):
+        inf = makeInferenceWithRules("{ :a :b ?x . } => { :new :stmt ?x } .")
+        implied = inf.infer(N3(":a :b :c ."))
+        self.assertGraphEqual(implied, N3(":new :stmt :c ."))
+
+    def testVarMatchesTwice(self):
+        inf = makeInferenceWithRules("{ :a :b ?x . } => { :new :stmt ?x } .")
+        implied = inf.infer(N3(":a :b :c, :d ."))
+        self.assertGraphEqual(implied, N3(":new :stmt :c, :d ."))
+
+    def testTwoRulesApplyIndependently(self):
+        inf = makeInferenceWithRules("""
+            { :a :b ?x . } => { :new :stmt ?x . } .
+            { :d :e ?y . } => { :new :stmt2 ?y . } .
+            """)
+        implied = inf.infer(N3(":a :b :c ."))
+        self.assertGraphEqual(implied, N3("""
+            :new :stmt :c .
+            """))
+        implied = inf.infer(N3(":a :b :c . :d :e :f ."))
+        self.assertGraphEqual(implied, N3("""
+            :new :stmt :c .
+            :new :stmt2 :f .
+            """))
+
+    def testOneRuleActivatesAnother(self):
+        inf = makeInferenceWithRules("""
+            { :a :b ?x . } => { :new :stmt ?x . } .
+            { ?y :stmt ?z . } => { :new :stmt2 ?y . } .
+            """)
+        implied = inf.infer(N3(":a :b :c ."))
+        self.assertGraphEqual(implied, N3("""
+            :new :stmt :c .
+            :new :stmt2 :new .
+            """))
+
+    def testRuleMatchesStaticStatement(self):
+        inf = makeInferenceWithRules("{ :a :b ?x . :a :b :c . } => { :new :stmt ?x } .")
+        implied = inf.infer(N3(":a :b :c  ."))
+        self.assertGraphEqual(implied, N3(":new :stmt :c ."))
+
+
+class TestVarLinksTwoStatements(WithGraphEqual):
+
+    def setUp(self):
+        self.inf = makeInferenceWithRules("{ :a :b ?x . :d :e ?x } => { :new :stmt ?x } .")
+
+    def testOnlyOneStatementPresent(self):
+        implied = self.inf.infer(N3(":a :b :c  ."))
+        self.assertGraphEqual(implied, N3(""))
+
+    def testObjectsConflict(self):
+        implied = self.inf.infer(N3(":a :b :c . :d :e :f ."))
+        self.assertGraphEqual(implied, N3(""))
+
+    def testObjectsAgree(self):
+        implied = self.inf.infer(N3(":a :b :c . :d :e :c ."))
+        self.assertGraphEqual(implied, N3(":new :stmt :c ."))
+
+
+class TestBnodeMatching(WithGraphEqual):
+
+    def testRuleBnodeBindsToInputBnode(self):
+        inf = makeInferenceWithRules("{ [ :a :b ] . } => { :new :stmt :here } .")
+        implied = inf.infer(N3("[ :a :b ] ."))
+        self.assertGraphEqual(implied, N3(":new :stmt :here ."))
+
+    def testRuleVarBindsToInputBNode(self):
+        inf = makeInferenceWithRules("{ ?z :a :b  . } => { :new :stmt :here } .")
+        implied = inf.infer(N3("[] :a :b ."))
+        self.assertGraphEqual(implied, N3(":new :stmt :here ."))
+
+
+class TestBnodeAliasingSetup(WithGraphEqual):
+
+    def setUp(self):
+        self.inf = makeInferenceWithRules("""
+          {
+            ?var0 :a ?x; :b ?y  .
+          } => {
+            :xVar :value ?x .
+            :yVar :value ?y .
+          } .
+          """)
+
+    def assertResult(self, actual):
+        self.assertGraphEqual(actual, N3("""
+          :xVar :value :x0, :x1 .
+          :yVar :value :y0, :y1 .
+        """))
+
+    def testMatchesDistinctStatements(self):
+        implied = self.inf.infer(N3("""
+          :stmt0 :a :x0; :b :y0 .
+          :stmt1 :a :x1; :b :y1 .
+        """))
+        self.assertResult(implied)
+
+    def testMatchesDistinctBnodes(self):
+        implied = self.inf.infer(N3("""
+          [ :a :x0; :b :y0 ] .
+          [ :a :x1; :b :y1 ] .
+        """))
+        self.assertResult(implied)
+
+    def testProdCase(self):
+        inf = makeInferenceWithRules('''
+            {
+                :AirQualitySensor :nameRemap [
+                    :sensorName ?sensorName;
+                    :measurementName ?measurement
+                    ] .
+            } => {
+                :a :b ?sensorName.
+                :d :e ?measurement.
+            } .
+        ''')
+        implied = inf.infer(
+            N3('''
+            :AirQualitySensor :nameRemap
+              [:sensorName "bme280_pressure"; :measurementName "pressure"],
+              [:sensorName "bme280_temperature"; :measurementName "temperature"] .
+        '''))
+
+        self.assertGraphEqual(implied, N3('''
+          :a :b "bme280_pressure", "bme280_temperature" .
+          :d :e "pressure", "temperature" .
+        '''))
+
+
+class TestBnodeGenerating(WithGraphEqual):
+
+    def testRuleBnodeMakesNewBnode(self):
+        inf = makeInferenceWithRules("{ [ :a :b ] . } => { [ :c :d ] } .")
+        implied = inf.infer(N3("[ :a :b ] ."))
+        ruleNode = list(inf.rules[0].rhsGraph)[0]
+        stmt0Node = list(implied)[0][0]
+        self.assertNotEqual(ruleNode, stmt0Node)
+
+    def testRuleBnodeMakesNewBnodesEachTime(self):
+        inf = makeInferenceWithRules("{ [ :a ?x ] . } => { [ :c :d ] } .")
+        implied = inf.infer(N3("[ :a :b, :e ] ."))
+        ruleNode = list(inf.rules[0].rhsGraph)[0]
+        stmt0Node = list(implied)[0][0]
+        stmt1Node = list(implied)[1][0]
+
+        self.assertNotEqual(ruleNode, stmt0Node)
+        self.assertNotEqual(ruleNode, stmt1Node)
+        self.assertNotEqual(stmt0Node, stmt1Node)
+
+
+class TestSelfFulfillingRule(WithGraphEqual):
+
+    def test1(self):
+        inf = makeInferenceWithRules("{ } => { :new :stmt :x } .")
+        self.assertGraphEqual(inf.infer(N3("")), N3(":new :stmt :x ."))
+        self.assertGraphEqual(inf.infer(N3(":any :any :any .")), N3(":new :stmt :x ."))
+
+    # def test2(self):
+    #     inf = makeInferenceWithRules("{ (2) math:sum ?x } => { :new :stmt ?x } .")
+    #     self.assertGraphEqual(inf.infer(N3("")), N3(":new :stmt 2 ."))
+
+    # @unittest.skip("too hard for now")
+    # def test3(self):
+    #     inf = makeInferenceWithRules("{ :a :b :c . :a :b ?x . } => { :new :stmt ?x } .")
+    #     self.assertGraphEqual(inf.infer(N3("")), N3(":new :stmt :c ."))
+
+
+class TestInferenceWithMathFunctions(WithGraphEqual):
+
+    def testBoolFilter(self):
+        inf = makeInferenceWithRules("{ :a :b ?x . ?x math:greaterThan 5 } => { :new :stmt ?x } .")
+        self.assertGraphEqual(inf.infer(N3(":a :b 3 .")), N3(""))
+        self.assertGraphEqual(inf.infer(N3(":a :b 5 .")), N3(""))
+        self.assertGraphEqual(inf.infer(N3(":a :b 6 .")), N3(":new :stmt 6 ."))
+
+    def testNonFiringMathRule(self):
+        inf = makeInferenceWithRules("{ :a :b ?x . (?x 1) math:sum ?y } => { :new :stmt ?y } .")
+        self.assertGraphEqual(inf.infer(N3("")), N3(""))
+
+    def testStatementGeneratingRule(self):
+        inf = makeInferenceWithRules("{ :a :b ?x . (?x) math:sum ?y } => { :new :stmt ?y } .")
+        self.assertGraphEqual(inf.infer(N3(":a :b 3 .")), N3(":new :stmt 3 ."))
+
+    def test2Operands(self):
+        inf = makeInferenceWithRules("{ :a :b ?x . (?x 1) math:sum ?y } => { :new :stmt ?y } .")
+        self.assertGraphEqual(inf.infer(N3(":a :b 3 .")), N3(":new :stmt 4 ."))
+
+    def test3Operands(self):
+        inf = makeInferenceWithRules("{ :a :b ?x . (2 ?x 2) math:sum ?y } => { :new :stmt ?y } .")
+        self.assertGraphEqual(inf.infer(N3(":a :b 2 .")), N3(":new :stmt 6 ."))
+
+    # def test0Operands(self):
+    #     inf = makeInferenceWithRules("{ :a :b ?x . () math:sum ?y } => { :new :stmt ?y } .")
+    #     self.assertGraphEqual(inf.infer(N3(":a :b 2 .")), N3(":new :stmt 0 ."))
+
+
+class TestInferenceWithCustomFunctions(WithGraphEqual):
+
+    def testAsFarenheit(self):
+        inf = makeInferenceWithRules("{ :a :b ?x . ?x room:asFarenheit ?f } => { :new :stmt ?f } .")
+        self.assertGraphEqual(inf.infer(N3(":a :b 12 .")), N3(":new :stmt 53.6 ."))
+
+    def testChildResource(self):
+        inf = makeInferenceWithRules("{ :a :b ?x . (:c ?x) room:childResource ?y .} => { :new :stmt ?y  } .")
+        self.assertGraphEqual(inf.infer(N3(':a :b "foo" .')), N3(":new :stmt <http://projects.bigasterisk.com/room/c/foo> ."))
+
+    def testChildResourceSegmentQuoting(self):
+        inf = makeInferenceWithRules("{ :a :b ?x . (:c ?x) room:childResource ?y .} => { :new :stmt ?y  } .")
+        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.> ."))
+
+
+class TestUseCases(WithGraphEqual):
+
+    def testSimpleTopic(self):
+        inf = makeInferenceWithRules('''
+            { ?msg :body "online" . } => { ?msg :onlineTerm :Online . } .
+            { ?msg :body "offline" . } => { ?msg :onlineTerm :Offline . } .
+
+            {
+            ?msg a :MqttMessage ;
+                :topic :foo;
+                :onlineTerm ?onlineness . } => {
+            :frontDoorLockStatus :connectedStatus ?onlineness .
+            } .
+        ''')
+
+        out = inf.infer(N3('[] a :MqttMessage ; :body "online" ; :topic :foo .'))
+        self.assertIn((ROOM['frontDoorLockStatus'], ROOM['connectedStatus'], ROOM['Online']), out)
+
+    def testTopicIsList(self):
+        inf = makeInferenceWithRules('''
+            { ?msg :body "online" . } => { ?msg :onlineTerm :Online . } .
+            { ?msg :body "offline" . } => { ?msg :onlineTerm :Offline . } .
+
+            {
+            ?msg a :MqttMessage ;
+                :topic ( "frontdoorlock" "status" );
+                :onlineTerm ?onlineness . } => {
+            :frontDoorLockStatus :connectedStatus ?onlineness .
+            } .
+        ''')
+
+        out = inf.infer(N3('[] a :MqttMessage ; :body "online" ; :topic ( "frontdoorlock" "status" ) .'))
+        self.assertIn((ROOM['frontDoorLockStatus'], ROOM['connectedStatus'], ROOM['Online']), out)
+
+    def testPerformance0(self):
+        inf = makeInferenceWithRules('''
+            {
+              ?msg a :MqttMessage;
+                :topic :topic1;
+                :bodyFloat ?valueC .
+              ?valueC math:greaterThan -999 .
+              ?valueC room:asFarenheit ?valueF .
+            } => {
+              :airQualityIndoorTemperature :temperatureF ?valueF .
+            } .
+        ''')
+        out = inf.infer(
+            N3('''
+            <urn:uuid:c6e1d92c-0ee1-11ec-bdbd-2a42c4691e9a> a :MqttMessage ;
+                :body "23.9" ;
+                :bodyFloat 2.39e+01 ;
+                :topic :topic1 .
+            '''))
+
+        vlit = cast(Literal, out.value(ROOM['airQualityIndoorTemperature'], ROOM['temperatureF']))
+        valueF = cast(Decimal, vlit.toPython())
+        self.assertAlmostEqual(float(valueF), 75.02)
+
+    def testPerformance1(self):
+        inf = makeInferenceWithRules('''
+            {
+              ?msg a :MqttMessage;
+                :topic ( "air_quality_indoor" "sensor" "bme280_temperature" "state" );
+                :bodyFloat ?valueC .
+              ?valueC math:greaterThan -999 .
+              ?valueC room:asFarenheit ?valueF .
+            } => {
+              :airQualityIndoorTemperature :temperatureF ?valueF .
+            } .
+        ''')
+        out = inf.infer(
+            N3('''
+            <urn:uuid:c6e1d92c-0ee1-11ec-bdbd-2a42c4691e9a> a :MqttMessage ;
+                :body "23.9" ;
+                :bodyFloat 2.39e+01 ;
+                :topic ( "air_quality_indoor" "sensor" "bme280_temperature" "state" ) .
+        '''))
+        vlit = cast(Literal, out.value(ROOM['airQualityIndoorTemperature'], ROOM['temperatureF']))
+        valueF = cast(Decimal, vlit.toPython())
+        self.assertAlmostEqual(float(valueF), 75.02)
+
+    def testEmitBnodes(self):
+        inf = makeInferenceWithRules('''
+            { ?s a :AirQualitySensor; :label ?name . } => {
+                [ a :MqttStatementSource;
+                :mqttTopic (?name "sensor" "bme280_temperature" "state") ] .
+            } .
+        ''')
+        out = inf.infer(N3('''
+            :airQualityOutdoor a :AirQualitySensor; :label "air_quality_outdoor" .
+        '''))
+        out.bind('', ROOM)
+        out.bind('ex', EX)
+        self.assertEqual(
+            out.serialize(format='n3'), b'''\
+@prefix : <http://projects.bigasterisk.com/room/> .
+@prefix ex: <http://example.com/> .
+@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix xml: <http://www.w3.org/XML/1998/namespace> .
+@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
+
+[] a :MqttStatementSource ;
+    :mqttTopic ( "air_quality_outdoor" "sensor" "bme280_temperature" "state" ) .
+
+''')
+
+    def testRemap(self):
+        inf = makeInferenceWithRules('''
+            {
+              ?sensor a :AirQualitySensor; :label ?name .
+              (:mqttSource ?name) :childResource ?base .
+            } => {
+              ?sensor :statementSourceBase ?base .
+            } .
+        ''')
+        out = inf.infer(
+            N3('''
+            :airQualityIndoor a :AirQualitySensor; :label "air_quality_indoor" .
+            :airQualityOutdoor a :AirQualitySensor; :label "air_quality_outdoor" .
+        '''), Path('/tmp/log.html'))
+        self.assertGraphEqual(
+            out,
+            N3('''
+            :airQualityIndoor  :statementSourceBase <http://projects.bigasterisk.com/room/mqttSource/air_quality_indoor> .
+            :airQualityOutdoor :statementSourceBase <http://projects.bigasterisk.com/room/mqttSource/air_quality_outdoor> .
+        '''))
+
+
+class TestListPerformance(WithGraphEqual):
+
+    def testList1(self):
+        inf = makeInferenceWithRules("{ :a :b (:e0) . } => { :new :stmt :here } .")
+        implied = inf.infer(N3(":a :b (:e0) ."))
+        self.assertGraphEqual(implied, N3(":new :stmt :here ."))
+
+    def testList2(self):
+        inf = makeInferenceWithRules("{ :a :b (:e0 :e1) . } => { :new :stmt :here } .")
+        implied = inf.infer(N3(":a :b (:e0 :e1) ."))
+        self.assertGraphEqual(implied, N3(":new :stmt :here ."))
+
+    def testList3(self):
+        inf = makeInferenceWithRules("{ :a :b (:e0 :e1 :e2) . } => { :new :stmt :here } .")
+        implied = inf.infer(N3(":a :b (:e0 :e1 :e2) ."))
+        self.assertGraphEqual(implied, N3(":new :stmt :here ."))
+
+    # def testList4(self):
+    #     inf = makeInferenceWithRules("{ :a :b (:e0 :e1 :e2 :e3) . } => { :new :stmt :here } .")
+    #     implied = inf.infer(N3(":a :b (:e0 :e1 :e2 :e3) ."))
+    #     self.assertGraphEqual(implied, N3(":new :stmt :here ."))