eclipse-rdf4j / rdf4j

Eclipse RDF4J: scalable RDF for Java
https://rdf4j.org/
BSD 3-Clause "New" or "Revised" License
365 stars 163 forks source link

Value-Inheritance is not respected as expected in SHACL #4941

Closed uahic closed 7 months ago

uahic commented 7 months ago

Current Behavior

As pointed out in the discussions #4939 section, it seems that following the inheritance ladder in a PropertyShape's sh:Class constraint is not implemented.

Expected Behavior

I would expect that I can simply use 'Base classes' as values in the sh:class constraint.

Steps To Reproduce

Data Graph


@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix example: <http://www.semanticweb.org/test/ontologies/2023/7/example-ontology#> .

# Drugs
example:Drug a rdfs:Class .
example:Coke a rdfs:Class ;
    rdfs:subClassOf example:Drug .

# Persons
example:Person a rdfs:Class .
example:Doctor a rdfs:Class ;
    rdfs:subClassOf example:Person .

# Relations
example:sniffs a rdfs:Property .

# "Instances"
example:Jack a example:Doctor ;
    example:sniffs example:Coke .

Shapes Graph

@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix example: <http://www.semanticweb.org/test/ontologies/2023/7/example-ontology#> .

example:DrugShape
    a sh:PropertyShape ;
    sh:path example:sniffs ;
    sh:class example:Drug ;  # <--- this does not work :(
    sh:name "consumer" .

example:PersonShape
    a sh:NodeShape ;
    sh:targetClass example:Person; 
    sh:property example:DrugShape ; 
    .

Version

4.3.10

Are you interested in contributing a solution yourself?

Perhaps?

Anything else?

No response

uahic commented 7 months ago

Ok it seems I can not use example:Jack example:sniffs example:Coke here but have to define one more layer

example:JacksCoke rdf:type example:Coke . (=> without subclass)

then it works. Either I have not understood RDF properly or this is a bug :- ) So this basically means a thing that is a subclass of some other thing is not accepted by have strictly to be only rdf:type of the other thing. Not sure if this is backed by the mathematical foundation of RDFS but it feels wrong to me

kenwenzel commented 7 months ago

The schema/ontology languages RDF Schema (RDFS) and OWL strictly separate ABox and TBox: https://en.wikipedia.org/wiki/Abox

Usually it is not possible to use a term at the same time as class and as instance. An example can be found here: https://www.w3.org/TR/owl2-new-features/#F12:_Punning

In your case you like to express an ABox statement by saying that Jack sniffs a particular instance of the class Drug which is Coke. The restriction in your property shape sh:class example:Drug does not work in this case as example:Coke is of type rdfs:Class and not example:Drug when Coke is declared as a sub-class of Drug and not as an instance of Drug.

You can solve this by using SKOS or something comparable to model drugs and their sub-concepts: https://www.w3.org/TR/skos-reference/

The key is that you shouldn't use rdfs:subClassOf to model the taxonomy of drugs if you like to use them as part of ABox statements.

hmottestad commented 7 months ago

@kenwenzel is right. The issue is that sh:class is used to constrain a value node to be an instances of a class. RDF4J does take inheritance into account when checking sh:class but the value node still needs to be an instance of a class on the inheritance tree, not an actual class.

I believe you can probably solve this using a reasoner and sh:in. You could probably also use a one-or-more path, but those are not supported by the RDF4J SHACL implementation yet.

hmottestad commented 7 months ago

Btw. I like to use the SHACL playground to test out stuff. Here is the shape and data you provided.

If you switch out:

example:Jack a example:Doctor ;
    example:sniffs example:Coke .

With:

example:Jack a example:Doctor ;
    example:sniffs [ a example:Coke ] .

Your data becomes valid.

uahic commented 7 months ago

Thanks @kenwenzel for the explanation, I really thought that there is actually not really a concept of 'classes' and 'instances' after reading through a couple of university lecture slides. After you have mentioned that I searched again and found this stackoverflow post to have a couple of nice examples and graphics to explain it to dummys such as myself :) https://stackoverflow.com/questions/24817607/why-must-rdfdatatype-subclass-rdfclass-in-rdf.

Not sure if SKOS is really what I am looking for (lack of expertise). In the real application I am dealing with space satellite components and how they are connected (network protocols, power supply etc. on a semi detailled level) and want to validate that the assembly is 'legal' and does not violate constraints.

What I really want to achieve in the application (publicly founded project) is as follows: Our RDF4J server has a pre-defined datagraph, containing the class Components (=things that do not work/make-sense standalone)

schumann:Component a rdfs:Class ;
    rdfs:label "Component" .

schumann:Supplier a rdfs:Class ;
    rdfs:label "Supplier" .

schumann:COMAT a schumann:Supplier .

and a couple of actual components you can buy on the market (like valves, batteries, sensors,... ). Like e.g.:

schumann:RW40 a rdfs:Class ;
    rdfs:subClassOf schumann:Component ;
    schumann:hasPhysicalLayer schumann:RS422 ;
    schumann:hasPropertyClass schumann:RW40_8yrsLifetime ;  
    4# could also probably just use :hasVolatage "14"^^uo:<someVoltageClass> here, hmh...
    schumann:hasPropertyClass schumann:RW40_SupplyVoltage14V ; 
    schumann:hasPropertyClass schumann:RW40_Torque4mNm ;
    schumann:hasSupplier schumann:COMAT ;
    **schumann:hasInterface schumann:SuperduperInterface** # <<--- not implemented yet
    schumann:hasTransportLayer schumann:NSP ;

then on the lowest level we would have namedGraphs for satellite modules (assemblies of multiple components). e.g.

GRAPH schumann:JacksModule
{
    schumann:JacksComp1 a schumann:RW40 .
    schumann:jacksComp2 a schumann:RaspberryPi .

    schumann:JacksComp1_RW40_Iface_1 a schumann:Interface .
    schumann:JacksComp1_RaspberryPi_Iface_1 a schumann:Interface .

    # or using an extra entity 'Interface' or blank node to specify more details on the connection
    schumann:JacksComp1_RW40_Iface_1 schumann:connectedTo schumann:JacksComp1_RaspberryPi_Iface_1 . 
    schumann:JacksComp1_RaspberryPi_Iface_1 schumann:connectedTo schumann:JacksComp1_RW40_Iface_1 . 
}

The ShapesGraph:


schumann:hasSupplierShape  # A supplier can also be supplied by someone else 
    a sh:PropertyShape ;
    sh:path schumann:hasSupplier ;
    sh:class schumann:Supplier ;
    sh:name "hasSupplier" .

schumann:SupplierShape 
    a sh:NodeShape ;
    sh:targetClass schumann:Supplier ;
    sh:property schumann:hasSupplierShape ;
    .

schumann:ComponentShape
    a sh:NodeShape ;
    sh:targetClass schumann:Component ;
    sh:property schumann:existsInDomainShape ;  
    sh:property schumann:hasApplicationLayerShape ;
    sh:property schumann:hasDataLayerShape ; 
    sh:property schumann:hasInterfaceShape ;
    sh:property schumann:hasNetworkLayerShape ;
    sh:property schumann:hasPhysicalLayerShape ;
    sh:property schumann:hasPropertyClassShape ;
    sh:property schumann:hasSecuritySublayerShape ;
    sh:property schumann:hasSupplierShape ;
    sh:property schumann:hasSyncChnlLayerShape ;
    sh:property schumann:hasTransportLayerShape ;
    sh:property schumann:hasNDAShape ;
    .

And we dont go further in this application to compose entire satellites for now. In such a namedGraph/per Module, we would then have actual instances of RW40 (1 or more of them) and other components that users can connect to each other if the interfaces defined on the Classlevel (schumann:RW40 etc. specifies the principle existence of such an interface) allows it. Another example is that the user can try to build a new Module (given existing components) but specifies custom constraints such as maximumWeight (=I somehow ideally can use SHACL-SPARQL to do that).

How does this relate to the drug example above? I was uncertain how to model the component hierarchy and if SHACL is even able to follow subclass relations as expected. Now I am still unsure if the connectivity patterns can be checked via SHACL (do I already need SPARQL sh:select/sh:sparql for this?) and computing a sum and compare it against a fixed value.

I know this is already out of context of this Github issue but if any of these things are not possible with SPARQL please give me a warning : -) thank you!