aboutsummaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
authorMatthias Baumgartner <dev@igsor.net>2023-01-15 21:00:12 +0100
committerMatthias Baumgartner <dev@igsor.net>2023-01-15 21:00:12 +0100
commit80a97bfa9f22d0d6dd25928fe1754a3a0d1de78a (patch)
tree30d30fb669d7b43d7324ef8027306c24c1ec1ac2 /test
parentccaee71e2b6135d3b324fe551c8652940b67aab3 (diff)
downloadbsfs-80a97bfa9f22d0d6dd25928fe1754a3a0d1de78a.tar.gz
bsfs-80a97bfa9f22d0d6dd25928fe1754a3a0d1de78a.tar.bz2
bsfs-80a97bfa9f22d0d6dd25928fe1754a3a0d1de78a.zip
Distance filter ast node
Diffstat (limited to 'test')
-rw-r--r--test/graph/test_resolve.py13
-rw-r--r--test/query/ast_test/test_filter_.py35
-rw-r--r--test/query/test_validator.py27
-rw-r--r--test/triple_store/sparql/test_distance.py61
-rw-r--r--test/triple_store/sparql/test_parse_filter.py50
-rw-r--r--test/triple_store/sparql/test_sparql.py17
6 files changed, 199 insertions, 4 deletions
diff --git a/test/graph/test_resolve.py b/test/graph/test_resolve.py
index 0861a53..0918b02 100644
--- a/test/graph/test_resolve.py
+++ b/test/graph/test_resolve.py
@@ -46,6 +46,13 @@ class TestFilter(unittest.TestCase):
bsfs:Feature rdfs:subClassOf bsfs:Array .
xsd:integer rdfs:subClassOf bsfs:Number .
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dimension "5"^^xsd:integer .
+
+ bse:colors rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Colors .
+
bse:comment rdfs:subClassOf bsfs:Predicate ;
rdfs:domain bsfs:Entity ;
rdfs:range xsd:string ;
@@ -147,12 +154,18 @@ class TestFilter(unittest.TestCase):
self.assertEqual(resolver(schema.node(ns.bsfs.Entity),
ast.filter.Has(ns.bse.comment)),
ast.filter.Has(ns.bse.comment))
+ # for sake of completeness: Distance
+ self.assertEqual(resolver(schema.node(ns.bsfs.Entity),
+ ast.filter.Any(ns.bse.colors, ast.filter.Distance([1,2,3,4,5], 1))),
+ ast.filter.Any(ns.bse.colors, ast.filter.Distance([1,2,3,4,5], 1)))
# route errors
self.assertRaises(errors.BackendError, resolver, schema.node(ns.bsfs.Tag),
ast.filter.Predicate(ns.bse.comment))
self.assertRaises(errors.BackendError, resolver, schema.node(ns.bsfs.Tag),
ast.filter.Any(ast.filter.PredicateExpression(), ast.filter.Equals('foo')))
self.assertRaises(errors.BackendError, resolver._one_of, ast.filter.OneOf(ast.filter.Predicate(ns.bsfs.Predicate)))
+ # for sake of coverage completeness: valid OneOf
+ self.assertIsNotNone(resolver._one_of(ast.filter.OneOf(ast.filter.Predicate(ns.bse.colors))))
# check schema consistency
self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag),
diff --git a/test/query/ast_test/test_filter_.py b/test/query/ast_test/test_filter_.py
index 4f69bdc..9eb92e2 100644
--- a/test/query/ast_test/test_filter_.py
+++ b/test/query/ast_test/test_filter_.py
@@ -15,7 +15,7 @@ from bsfs.utils import URI
from bsfs.query.ast.filter_ import _Expression, FilterExpression, PredicateExpression
from bsfs.query.ast.filter_ import _Branch, Any, All
from bsfs.query.ast.filter_ import _Agg, And, Or
-from bsfs.query.ast.filter_ import Not, Has
+from bsfs.query.ast.filter_ import Not, Has, Distance
from bsfs.query.ast.filter_ import _Value, Is, Equals, Substring, StartsWith, EndsWith
from bsfs.query.ast.filter_ import _Bounded, LessThan, GreaterThan
from bsfs.query.ast.filter_ import Predicate, OneOf
@@ -284,6 +284,39 @@ class TestValue(unittest.TestCase):
self.assertEqual(cls(f).value, f)
+class TestDistance(unittest.TestCase):
+ def test_essentials(self):
+ ref = (1,2,3)
+ # comparison
+ self.assertEqual(Distance(ref, 3), Distance(ref, 3))
+ self.assertEqual(hash(Distance(ref, 3)), hash(Distance(ref, 3)))
+ # comparison respects type
+ self.assertNotEqual(Distance(ref, 3), FilterExpression())
+ self.assertNotEqual(hash(Distance(ref, 3)), hash(FilterExpression()))
+ # comparison respects reference
+ self.assertNotEqual(Distance((1,2,3), 3, False), Distance((1,2), 3, False))
+ self.assertNotEqual(hash(Distance((1,2,3), 3, False)), hash(Distance((1,2), 3, False)))
+ self.assertNotEqual(Distance((1,2,3), 3, False), Distance((1,5,3), 3, False))
+ self.assertNotEqual(hash(Distance((1,2,3), 3, False)), hash(Distance((1,5,3), 3, False)))
+ # comparison respects threshold
+ self.assertNotEqual(Distance((1,2,3), 3, False), Distance((1,2,3), 3.1, False))
+ self.assertNotEqual(hash(Distance((1,2,3), 3, False)), hash(Distance((1,2,3), 3.1, False)))
+ # comparison respects strict flag
+ self.assertNotEqual(Distance((1,2,3), 3, False), Distance((1,2,3), 3, True))
+ self.assertNotEqual(hash(Distance((1,2,3), 3, False)), hash(Distance((1,2,3), 3, True)))
+ # string conversion
+ self.assertEqual(str(Distance(ref, 3, False)), 'Distance((1, 2, 3), 3.0, False)')
+ self.assertEqual(repr(Distance(ref, 3, False)), 'Distance((1, 2, 3), 3.0, False)')
+
+ def test_members(self):
+ self.assertEqual(Distance((1,2,3), 3, False).reference, (1,2,3))
+ self.assertEqual(Distance((3,2,1), 3, False).reference, (3,2,1))
+ self.assertEqual(Distance((1,2,3), 3, False).threshold, 3.0)
+ self.assertEqual(Distance((1,2,3), 53.45, False).threshold, 53.45)
+ self.assertEqual(Distance((1,2,3), 3, False).strict, False)
+ self.assertEqual(Distance((1,2,3), 3, True).strict, True)
+
+
class TestBounded(unittest.TestCase):
def test_essentials(self):
# comparison respects type
diff --git a/test/query/test_validator.py b/test/query/test_validator.py
index 63ead52..dc9d913 100644
--- a/test/query/test_validator.py
+++ b/test/query/test_validator.py
@@ -38,6 +38,15 @@ class TestFilter(unittest.TestCase):
bsfs:Feature rdfs:subClassOf bsfs:Array .
xsd:integer rdfs:subClassOf bsfs:Number .
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dimension "5"^^xsd:integer ;
+ bsfs:dtype bsfs:f32 .
+
+ bse:color rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Node ;
+ rdfs:range bsfs:Colors ;
+ bsfs:unique "true"^^xsd:boolean .
+
bse:comment rdfs:subClassOf bsfs:Predicate ;
rdfs:domain bsfs:Node ;
rdfs:range xsd:string ;
@@ -88,6 +97,7 @@ class TestFilter(unittest.TestCase):
),
ast.filter.Not(ast.filter.Any(ns.bse.comment,
ast.filter.Not(ast.filter.Equals('hello world')))),
+ ast.filter.Any(ns.bse.color, ast.filter.Distance([1,2,3,4,5], 3)),
)))))
# invalid paths raise consistency error
self.assertRaises(errors.ConsistencyError, self.validate, self.schema.node(ns.bsfs.Entity),
@@ -257,6 +267,23 @@ class TestFilter(unittest.TestCase):
self.assertIsNone(self.validate._bounded(self.schema.literal(ns.xsd.integer), ast.filter.LessThan(0)))
self.assertIsNone(self.validate._bounded(self.schema.literal(ns.xsd.integer), ast.filter.GreaterThan(0)))
+ def test_distance(self):
+ # type must be a literal
+ self.assertRaises(errors.ConsistencyError, self.validate._distance, self.schema.node(ns.bsfs.Node),
+ ast.filter.Distance([1,2,3], 1, False))
+ # type must be a feature
+ self.assertRaises(errors.ConsistencyError, self.validate._distance, self.schema.literal(ns.bsfs.Array),
+ ast.filter.Distance([1,2,3], 1, False))
+ # type must be in the schema
+ self.assertRaises(errors.ConsistencyError, self.validate._distance, self.schema.literal(ns.bsfs.Feature).child(ns.bsfs.Invalid),
+ ast.filter.Distance([1,2,3], 1, False))
+ # FIXME: reference must be a numpy array
+ # reference must have the correct dimension
+ self.assertRaises(errors.ConsistencyError, self.validate._distance, self.schema.literal(ns.bsfs.Colors),
+ ast.filter.Distance([1,2,3], 1, False))
+ # FIXME: reference must have the correct dtype
+ # distance accepts correct expressions
+ self.assertIsNone(self.validate._distance(self.schema.literal(ns.bsfs.Colors), ast.filter.Distance([1,2,3,4,5], 1, False)))
## main ##
diff --git a/test/triple_store/sparql/test_distance.py b/test/triple_store/sparql/test_distance.py
new file mode 100644
index 0000000..0659459
--- /dev/null
+++ b/test/triple_store/sparql/test_distance.py
@@ -0,0 +1,61 @@
+"""
+
+Part of the bsfs test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import numpy as np
+import unittest
+
+# objects to test
+from bsfs.triple_store.sparql import distance
+
+
+## code ##
+
+class TestDistance(unittest.TestCase):
+
+ def test_euclid(self):
+ # self-distance is zero
+ self.assertEqual(distance.euclid([1,2,3,4], [1,2,3,4]), 0.0)
+ # accepts list-like arguments
+ self.assertAlmostEqual(distance.euclid([1,2,3,4], [2,3,4,5]), 2.0, 3)
+ self.assertAlmostEqual(distance.euclid((1,2,3,4), (2,3,4,5)), 2.0, 3)
+ # dimension can vary
+ self.assertAlmostEqual(distance.euclid([1,2,3], [2,3,4]), 1.732, 3)
+ self.assertAlmostEqual(distance.euclid([1,2,3,4,5], [2,3,4,5,6]), 2.236, 3)
+ # vector can be zero
+ self.assertAlmostEqual(distance.euclid([0,0,0], [1,2,3]), 3.742, 3)
+
+ def test_cosine(self):
+ # self-distance is zero
+ self.assertEqual(distance.cosine([1,2,3,4], [1,2,3,4]), 0.0)
+ # accepts list-like arguments
+ self.assertAlmostEqual(distance.cosine([1,2,3,4], [4,3,2,1]), 0.333, 3)
+ self.assertAlmostEqual(distance.cosine((1,2,3,4), (4,3,2,1)), 0.333, 3)
+ # dimension can vary
+ self.assertAlmostEqual(distance.cosine([1,2,3], [3,2,1]), 0.286, 3)
+ self.assertAlmostEqual(distance.cosine([1,2,3,4,5], [5,4,3,2,1]), 0.364, 3)
+ # vector can be zero
+ self.assertAlmostEqual(distance.cosine([0,0,0], [1,2,3]), 1.0, 3)
+
+ def test_manhatten(self):
+ # self-distance is zero
+ self.assertEqual(distance.manhatten([1,2,3,4], [1,2,3,4]), 0.0)
+ # accepts list-like arguments
+ self.assertAlmostEqual(distance.manhatten([1,2,3,4], [2,3,4,5]), 4.0, 3)
+ self.assertAlmostEqual(distance.manhatten((1,2,3,4), (2,3,4,5)), 4.0, 3)
+ # dimension can vary
+ self.assertAlmostEqual(distance.manhatten([1,2,3], [2,3,4]), 3.0, 3)
+ self.assertAlmostEqual(distance.manhatten([1,2,3,4,5], [2,3,4,5,6]), 5.0, 3)
+ # vector can be zero
+ self.assertAlmostEqual(distance.manhatten([0,0,0], [1,2,3]), 6.0, 3)
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/triple_store/sparql/test_parse_filter.py b/test/triple_store/sparql/test_parse_filter.py
index 5c16f11..8764535 100644
--- a/test/triple_store/sparql/test_parse_filter.py
+++ b/test/triple_store/sparql/test_parse_filter.py
@@ -42,6 +42,15 @@ class TestParseFilter(unittest.TestCase):
xsd:integer rdfs:subClassOf bsfs:Number .
bsfs:URI rdfs:subClassOf bsfs:Literal .
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dimension "4"^^xsd:integer ;
+ bsfs:dtype xsd:integer ;
+ bsfs:distance bsfs:euclidean .
+
+ bse:colors rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Colors .
+
bse:comment rdfs:subClassOf bsfs:Predicate ;
rdfs:domain bsfs:Node ;
rdfs:range xsd:string ;
@@ -74,9 +83,6 @@ class TestParseFilter(unittest.TestCase):
''')
- # parser instance
- self.parser = Filter(self.schema)
-
# graph to test queries
self.graph = rdflib.Graph()
# schema hierarchies
@@ -117,6 +123,13 @@ class TestParseFilter(unittest.TestCase):
# image iso
self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.URIRef(ns.bse.iso), rdflib.Literal(1234, datatype=rdflib.XSD.integer)))
self.graph.add((rdflib.URIRef('http://example.com/image#4321'), rdflib.URIRef(ns.bse.iso), rdflib.Literal(4321, datatype=rdflib.XSD.integer)))
+ # color features
+ self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.colors), rdflib.Literal([1,2,3,4], datatype=rdflib.URIRef(ns.bsfs.Colors))))
+ self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.colors), rdflib.Literal([4,3,2,1], datatype=rdflib.URIRef(ns.bsfs.Colors))))
+ self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.URIRef(ns.bse.colors), rdflib.Literal([3,4,2,1], datatype=rdflib.URIRef(ns.bsfs.Colors))))
+
+ # parser instance
+ self.parser = Filter(self.graph, self.schema)
def test_routing(self):
@@ -617,6 +630,37 @@ class TestParseFilter(unittest.TestCase):
{'http://example.com/tag#1234'})
+ def test_distance(self):
+ # node colors distance to [2,4,3,1]
+ # entity#1234 [1,2,3,4] 3.742
+ # entity#4321 [4,3,2,1] 2.449
+ # image#1234 [3,4,2,1] 1.414
+
+ # _distance expects a feature
+ self.assertRaises(errors.BackendError, self.parser._distance, self.schema.node(ns.bsfs.Entity), ast.filter.Distance([1,2,3,4], 1), '')
+ # reference must have the correct dimension
+ self.assertRaises(errors.ConsistencyError, self.parser._distance, self.schema.literal(ns.bsfs.Colors), ast.filter.Distance([1,2,3], 1), '')
+ self.assertRaises(errors.ConsistencyError, self.parser._distance, self.schema.literal(ns.bsfs.Colors), ast.filter.Distance([1,2,3,4,5], 1), '')
+ # _distance respects threshold
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([2,4,3,1], 4)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321', 'http://example.com/image#1234'})
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([2,4,3,1], 3)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321', 'http://example.com/image#1234'})
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([2,4,3,1], 2)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/image#1234'})
+ # result set can be empty
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([2,4,3,1], 1)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set())
+ # _distance respects strict
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([1,2,3,4], 0, False)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234'})
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([1,2,3,4], 0, True)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set())
+
def test_one_of(self):
# _one_of expects a node
self.assertRaises(errors.BackendError, self.parser._one_of,
diff --git a/test/triple_store/sparql/test_sparql.py b/test/triple_store/sparql/test_sparql.py
index 1f56a7e..435ca28 100644
--- a/test/triple_store/sparql/test_sparql.py
+++ b/test/triple_store/sparql/test_sparql.py
@@ -392,6 +392,23 @@ class TestSparqlStore(unittest.TestCase):
class Foo(): pass
self.assertRaises(TypeError, setattr, store, 'schema', Foo())
+ # cannot define features w/o known distance function
+ invalid = bsc.from_string('''
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix xsd: <http://www.w3.org/2001/XMLSchema#>
+ prefix bsfs: <http://bsfs.ai/schema/>
+ prefix bse: <http://bsfs.ai/schema/Entity#>
+
+ bsfs:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array .
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dimension "4"^^xsd:integer ;
+ bsfs:distance bsfs:foobar .
+
+ ''')
+ self.assertRaises(ValueError, setattr, store, 'schema', invalid)
+
# cannot migrate to incompatible schema
invalid = bsc.from_string('''
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>