jenaowljena-rules

OWL reasoning: alternative to NOT


I'm doing a project in JENA using OWL rules.

In my ontology I have an entity named PEGI_RATING. A PEGI_RATING can have multiple descriptors.

Example:

<http://localhost:2020/PEGI_RATING/6>
      a       vocab:PEGI_RATING ;
      rdfs:label "PEGI_RATING #6" ;
      vocab:PEGI_RATING_age
              16 ;
      vocab:PEGI_RATING_has_PEGI_CONTENT_DESCRIPTOR
              <http://localhost:2020/PEGI_CONTENT_DESCRIPTOR/Online> , 
              <http://localhost:2020/PEGI_CONTENT_DESCRIPTOR/Violence> , 
              <http://localhost:2020/PEGI_CONTENT_DESCRIPTOR/Bad_Language> ;
      vocab:PEGI_RATING_ratingId
              6 .

Now, I want to write a rule in OWL that attaches an age to each PEGI_RATING. I know it is there already, but it is required to show that I know how to use the reasoner.

Now, a content descriptor has an age attached to it. I do this with the following rule:

[AgeLimit:
  (?descr rdf:type vocab:PEGI_CONTENT_DESCRIPTOR)
  (?descr vocab:PEGI_CONTENT_DESCRIPTOR_contentDescriptor "Sex")
  ->
  (?descr vocab:PEGI_CONTENT_DESCRIPTOR_inf_age_limit "16"^^xsd:integer)
]

So finally I have a VIDEO_GAME which has a PEGI_RATING id. Example:

<http://localhost:2020/VIDEOGAME/Grand_Theft_Auto_IV>
      a       vocab:VIDEOGAME ;
      rdfs:label "VIDEOGAME #Grand Theft Auto IV" ;
      vocab:VIDEOGAME_EsrbRatingCategoryCategory
              <http://localhost:2020/ESRB_RATING_CATEGORY/M> ;
      vocab:VIDEOGAME_gameplayRulesGameplayRulesId
              <http://localhost:2020/GAMEPLAY_RULES/3> ;
      vocab:VIDEOGAME_has_SIDE_GOAL
              <http://localhost:2020/SIDE_GOAL/Complete_all_missions.> ;
      vocab:VIDEOGAME_onlineMultiplayer
              "false"^^xsd:boolean ;
      vocab:VIDEOGAME_pegiRatingRatingId
              <http://localhost:2020/PEGI_RATING/3> ;
      vocab:VIDEOGAME_summary
              "For Niko Bellic, fresh off the boat from Europe, it is the hope he can escape his past. For his cousin, Roman, it is the vision that together they can find fortune in Liberty City, gateway to the land of opportunity. As they slip into debt and are dragged into a criminal underworld by a series of shysters, thieves and sociopaths, they discover that the reality is very different from the dream in a city that worships money and status, and is heaven for those who have them and a living nightmare for those who don't." ;
      vocab:VIDEOGAME_title
              "Grand Theft Auto IV" .

I want to create a rule that determines the maximum age that PEGI_CONTENT_DESCRIPTORs that are attached to the PEGI_RATING of the VIDEOGAME.

In Prolog I would do something like this:

 [Age:
  (?gameRelease rdf:type vocab:GAME_RELEASE)
  (?gameRelease vocab:GAME_RELEASE_videogameTitle ?game)
  (?game vocab:VIDEOGAME_pegiRatingRatingId ?pegiID)
  (?pegiID vocab:PEGI_RATING_has_PEGI_CONTENT_DESCRIPTOR ?descriptor)
  (?descriptor vocab:PEGI_CONTENT_DESCRIPTOR_inf_age_limit ?age)
  (?descriptor vocab:PEGI_CONTENT_DESCRIPTOR_inf_age_limit ?age2)
  not(lessThan(?age, ?age2))
  ->
  (?game vocab:VIDEOGAME_inf_minimumAge ?age)]

But since OWL does not appear to have negation by failure I'm stumped on how to solve it.

So far I have tried the following rule without succes:

[Age:
  (?gameRelease rdf:type vocab:GAME_RELEASE)
  (?gameRelease vocab:GAME_RELEASE_videogameTitle ?game)
  (?game vocab:VIDEOGAME_pegiRatingRatingId ?pegiID)
  (?pegiID vocab:PEGI_RATING_has_PEGI_CONTENT_DESCRIPTOR ?descriptor)
  (?descriptor vocab:PEGI_CONTENT_DESCRIPTOR_inf_age_limit ?age)
  (?descriptor vocab:PEGI_CONTENT_DESCRIPTOR_inf_age_limit ?age2)
  greaterThan(?age, ?age2)
  ->
  (?game vocab:VIDEOGAME_inf_minimumAge ?age)]

FINAL RESULT

So finally we have a working function. It does exactly what is supposed to do. However..

First of all, we have a number of rules in Jena format that add a specific age to each PEGI_CONTENT_DESCRIPTOR. This is purely for didactic reasons (i.e., showing we can use a reasoner). This works. When I add these rules I can execute SPARQL queries against my model and I get the proper values. When i do a write of my model (as Rob Hall indicated in his example) the PEGI_CONTENT_DESCRIPTORs have indeed an age. They look like this:

<http://local.host.com:2020/PEGI_CONTENT_DESCRIPTOR/Violence>
      a       vocab:PEGI_CONTENT_DESCRIPTOR ;
      rdfs:label "PEGI_CONTENT_DESCRIPTOR #Violence" ;
      vocab:PEGI_CONTENT_DESCRIPTOR_contentDescriptor
              "Violence" ;
      vocab:PEGI_CONTENT_DESCRIPTOR_explanation
              "May contain scenes of people getting injured or dying, often by use of weapons, whether realistically or in a fantastical or cartoonish manner. Also may contain gore and blood-letting." ;
      vocab:PEGI_CONTENT_DESCRIPTOR_inf_age_limit
              18 .

Remember that a video game has a PEGI rating Id: ?game vocab:VIDEOGAME_pegiRatingRatingId ?pegiId.

We want to execute the Jena Builtin as follows:

minimumPegiAge(?pegiID, ?age)

To this end we have the function below. It actually works. However, for some strange reason, context.find(pegiID, has_descriptor.asNode(), Node.ANY); does not seem to iterate over two particular PEGI_DESCRIPTORs. Namely Sex and Violence. As metioned, they are present in the inferred model and they are returned from SPARQL queries. Could we be dealing with a bug? Or are we missing something?

final Property has_age_limit =
        ResourceFactory.createProperty("http://localhost:2020/vocab/PEGI_CONTENT_DESCRIPTOR_inf_age_limit");
final Property has_descriptor =
        ResourceFactory.createProperty("http://localhost:2020/vocab/PEGI_RATING_has_PEGI_CONTENT_DESCRIPTOR");
// Create and Register a Builtin for Jena's rule system.
BuiltinRegistry.theRegistry.register(new BaseBuiltin() {
    @Override
    public String getName() {
        return "minPegiAge";
    }
    @Override
    public boolean bodyCall(final Node[] args, final int length, final com.hp.hpl.jena.reasoner.rulesys.RuleContext context) {
        checkArgs(length, context);
        final Node pegiID = getArg(0, args, context);
        if( !getArg(1, args, context).isVariable() ){
            return false;
        }

        // Should get all the descriptors for this PegiID.
        ClosableIterator<Triple> x = context.find(pegiID,  has_descriptor.asNode(),  Node.ANY);

        // Iterate over them.
        final Iterator<Node> results = 
                new NiceIterator<Triple>()
                .andThen(x) // Get all the descriptors
                .mapWith(new Map1<Triple,Node>(){
                    @Override
                    public Node map1(Triple o) {
                        // o is a triple
                        // These triples are descriptors
                        // We need to get the age for these descriptors
                        System.out.println(o);
                        return o.getObject();
                    }});

        if( !results.hasNext() ) {
            return false;
        }

        Node min = null;

        while(results.hasNext()) {
            final Node pegiContentDescriptor = results.next();
            System.out.println("DESCRIPTION: " + pegiContentDescriptor.toString());

            ClosableIterator<Triple> y = context.find(pegiContentDescriptor,  has_age_limit.asNode(),  Node.ANY);

             // Iterate over them.
            final Iterator<Node> singleAge = 
                    new NiceIterator<Triple>()
                    .andThen(y) // Get all the descriptors
                    .mapWith(new Map1<Triple,Node>(){
                        @Override
                        public Node map1(Triple o) {
                            // o is a triple
                            // These triples are descriptors
                            // We need to get the age for these descriptors                                 
                            return o.getObject();
                        }});

            if (singleAge.hasNext()) {
                Node age = singleAge.next();                        
                System.out.println("AGE: " + age.getLiteralValue());

                if (min == null) {
                    min = age;
                } else {
                    if (Util.compareTypedLiterals(min, age) < 0) {                              
                        min = age;
                    }
                }               
            }
        }

        if (min == null) {
            System.out.println("GEEN MINIMUM AGE GEVONDEN!");
        } else {
            System.out.println("MINIMUM: " + min.getLiteralValue());
        }

        context.getEnv().bind(getArg(1, args, context), min);
        return true;
    }
});
    // Load TTL-file (full db dump!)
    // Note: make sure the path containing the ttl file does not contain strange characters :D
    //       "-" and maybe spaces are not allowed
    model = ModelFactory.createDefaultModel();

    model.read(getClass().getResourceAsStream("/trivial-mapping-dump.ttl"), null, "TURTLE");

    // Load the rules.
    List<Rule> rules = Rule.rulesFromURL(getClass().getResource("/rules.txt").toString());

    // Let the reasoner.. reason!
    // Then add the triples existing due to rule firings to our base graph
    GenericRuleReasoner r = new GenericRuleReasoner(rules);
    r.setOWLTranslation(true);               // not needed in RDFS case
    r.setTransitiveClosureCaching(true);
    r.setMode(GenericRuleReasoner.HYBRID);
    InfModel infmodel = ModelFactory.createInfModel(r, model);
    model.add(infmodel.getDeductionsModel());

}

Solution

  • This is very similar to an existing question: Giving array as parameter to jena builtin. Before I begin, it is useful to note that identifying this element using a SPARQL query is extremely easy.

    In Jena, you can implement a rule similar to the following:

    [Age: 
      (?game urn:ex:hasRating ?pegiID) 
      minPegiAge(?pegiID ?age) 
      -> 
      (?game urn:ex:age ?age)]
    

    BEGIN EDIT

    It is extremely important that your rule begins with some generic triple pattern and not with the custom builtin (minPegiAge in this case). I've run into a problem where the RuleContext supplied to my builtin returns nothing from RuleContext#find(...). Additionally, the InfGraph (as well as Graph) for my rule context are both empty graphs that are not associated with the my actual InfModel. Once the rule is changed to include some generic triple pattern as the starting pattern, then the InfGraph associated with the RuleContext is the same InfGraph which your InfModel will return.

    END EDIT

    This requires that you then implement a Jena Builtin to calculate the minimum. Within the Builtin, you would need to use the available RuleContext in order to explore your graph and get the things which you need to explore the minimum of. The following example creates a builtin that pulls the minimum value for a particular datatype property.

    // These properties will be used in the example, I define them for
    // convenience here.
    final Property hasRating = ResourceFactory.createProperty("urn:ex:hasRating");
    final Property age = ResourceFactory.createProperty("urn:ex:age");
    
    // Create and Register a Builtin for Jena's rule system.
    BuiltinRegistry.theRegistry.register(new BaseBuiltin() {
        @Override
        public String getName() {
            return "minPegiAge";
        }
        @Override
        public boolean bodyCall( final Node[] args, final int length, final RuleContext context) {
            checkArgs(length, context);
            final Node rating = getArg(0, args, context);
            if( !getArg(1, args, context).isVariable() ){
                return false;
            }
    
            final Iterator<Node> results = 
                    new NiceIterator<Triple>()
                    .andThen(context.find(rating, age.asNode(), Node.ANY))
                    .mapWith(new Map1<Triple,Node>(){
                        @Override
                        public Node map1(Triple o) {
                            return o.getObject();
                        }});
            if( !results.hasNext() ) {
                return false;
            }
    
            Node min = results.next();
            while(results.hasNext()) {
                final Node val = results.next();
                if( Util.compareTypedLiterals(val, min) < 0 ) {
                    min = val;
                }
            }
            context.getEnv().bind(getArg(1, args, context), min);
            return true;
        }
    
    });
    
    // Construct some sample data for this simplified version of
    // your example scenario.
    final Model rawData = ModelFactory.createDefaultModel();
    final Resource game = rawData.createResource("urn:ex:theGame");
    final Resource rating = rawData.createResource("urn:ex:theRating");
    game.addProperty(hasRating, rating);
    rating.addLiteral(age, 15);
    rating.addLiteral(age, 14);
    
    // Construct a simplified version of the rule that you use
    // in order to identify when the minimum age needs to be
    // detected.
    final String rules = 
            "[Age: \n"+
            "  (?game urn:ex:hasRating ?pegiID) \n"+
            "  minPegiAge(?pegiID ?age) \n"+
            "  -> \n"+
            "  (?game urn:ex:age ?age)]";
    
    
    final Reasoner reasoner;
    try( final BufferedReader src = new BufferedReader(new StringReader(rules)) ) {
      reasoner = new GenericRuleReasoner(Rule.parseRules(Rule.rulesParserFromReader(src)));
    }
    final InfModel inf = ModelFactory.createInfModel(reasoner, rawData);
    
    // Write the model, now including a minimum age triple associated with
    // the game rather than the various pe
    inf.write(System.out, "TTL");