Coverage for lode / reader / logic / owl_logic.py: 71%
371 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-03-25 15:05 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-03-25 15:05 +0000
1# owl_logic.py
2from rdflib import Graph, URIRef, Node, Literal as RDFlibLiteral, BNode
3from rdflib.namespace import RDF, RDFS, OWL, SKOS, XSD
4from rdflib.collection import Collection as RDFLibCollection
6from lode.models import *
7from lode.reader.logic.base_logic import BaseLogic
10class OwlLogic(BaseLogic):
11 """
12 OWL-specific parsing logic.
14 Extends BaseLogic with:
15 - Phase 1 BNode classification for OWL restrictions and truth functions
16 - OWL defaults for domain, range (owl:Thing) and Individual type
17 - Handlers for property characteristics, cardinality, truth functions,
18 quantifiers, and group axioms (AllDisjointClasses, AllDifferent, etc.)
19 - Property type inference for properties not explicitly typed in the artefact
20 """
22 # _get_allowed_namespaces: ereditato da BaseLogic, legge config YAML
24 # ========== HOOK per resolve custom OWL ==========
26 def _pre_resolve_hook(self, python_class: type, id: Node) -> type | None:
27 """
28 OWL override: if the URI is already cached with a non-Individual type,
29 return that type to prevent silent downcasting during MRO resolution.
30 Returns None to fall through to the default MRO walk.
31 """
32 if id and id in self._instance_cache:
33 for existing in self._instance_cache[id]:
34 if type(existing) is not Individual:
35 return type(existing)
36 return None
38 # ========== READER PHASES ==========
40 def phase1_classify_from_predicates(self):
41 """Scans the RDF graph for subjects of mapped predicates.
42 - BNodes with inferred_class predicates -> classified and registered
43 - URIRefs with any mapped predicate but no rdf:type -> registered with
44 the inferred type from classify_by_predicate (only if unambiguous)
45 """
47 classified = {}
49 # Iterate ALL mapped predicates
50 for pred in self._property_mapping:
51 for uri in self.graph.subjects(pred, None):
52 if uri in self._instance_cache:
53 continue
54 python_class = self._strategy.classify_by_predicate(uri, self.graph)
55 if not python_class:
56 continue
57 # Goes for restrictions
58 if isinstance(uri, BNode):
59 if uri not in classified:
60 classified[uri] = python_class
61 # Goes for any other URI
62 elif isinstance(uri, URIRef):
63 self.get_or_create(uri, python_class, populate=False)
65 # classify recursively restrictions
66 self._classify_nested(classified, self._strategy.get_classifier_predicates())
68 # creates classified restrictions after recursion
69 for uri, py_class in classified.items():
70 self.get_or_create(uri, py_class, populate=False)
72 def phase2_create_from_types(self):
73 """Creates instances from rdf:type triples mapped in the config (is: class).
74 For each mapped rdf:type, uses the target_class field to resolve the
75 Python class to instantiate, then applies any immediate static setters defined in the config entry. Subjects whose rdf:type is not covered by the config are instantiated as Individual, provided they are URIRefs not yet in the cache."""
77 type_mapping = self._strategy.get_type_mapping()
79 for rdf_type, config in type_mapping.items():
80 py_class = config.get('target_class')
81 if not py_class:
82 continue
84 for uri in self.graph.subjects(RDF.type, rdf_type):
85 self.get_or_create(uri, py_class, populate=False)
87 # apply setters immediately
88 if 'setters' in config:
89 for instance in self._instance_cache.get(uri, set()):
90 self._apply_setters_immediate(instance, config['setters'])
92 # Subjects with an rdf:type not mapped in config -> Individual
93 for s, o in self.graph.subject_objects(RDF.type):
94 if o not in type_mapping and not isinstance(s, BNode) and s not in self._instance_cache:
95 self.get_or_create(s, Individual, populate=True)
97 def phase3_populate_properties(self):
98 """Iterates over all cached instances and populates their properties
99 by dispatching each predicate-object pair through the configured
100 setters and handlers defined in the property mapping.
101 """
103 for uri in list(self._instance_cache.keys()):
104 for instance in list(self._instance_cache[uri]):
105 self.populate_instance(instance, uri)
107 def phase4_process_group_axioms(self):
108 """Processes group axioms defined in the enricher section of the config.
109 For each axiom type, retrieves all matching subjects from the graph
110 and dispatches them to the handler specified in the config entry.
111 """
113 axioms = self._strategy.get_group_axioms()
114 for axiom_type, handler_name in axioms.items():
115 for uri in self.graph.subjects(RDF.type, axiom_type):
116 # handler existence guaranteed by _validate_handlers (defined in base_logic)
117 getattr(self, handler_name)(uri)
119 def phase5_fallback(self):
120 """
121 Classifies or reclassifies any entity whose concrete type could not be
122 determined in earlier phases, in particular generic Property instances
123 whose subtype (Relation, Attribute, Annotation) is not explicitly
124 asserted in the semantic artefact via rdf:type (handled by phase 2). It uses _infer_property_type to resolve the concrete subtype.
126 After reclassification, applies OWL defaults (e.g., domain, range, thing) to all instances via _enrich_or_apply_owl_defaults.
127 """
129 for uri, instances in list(self._instance_cache.items()):
130 for instance in list(instances):
132 if type(instance) is Property:
133 # Check if a more specific Property subclass already exists for this URI
135 has_concrete = any(
136 type(i) in (Relation, Attribute, Annotation)
137 for i in instances if i is not instance
138 )
140 if has_concrete:
141 # Remove generic Property, keep the concrete one
142 self._instance_cache[uri].discard(instance)
143 else:
144 # No concrete type found — infer and reclassify
145 inferred = self._infer_property_type(instance)
146 new = inferred()
147 new.__dict__.update(instance.__dict__)
148 self._instance_cache[uri].discard(instance)
149 self._instance_cache[uri].add(new)
150 if instance in self._triples_map:
151 self._triples_map[new] = self._triples_map.pop(instance)
152 self.populate_instance(new, uri)
154 self._enrich_or_apply_owl_defaults(instance, uri)
156 if instance.__class__ == DatatypeRestriction:
157 print('DEBUG::', vars(instance))
158 for constraint, value in zip(instance.get_has_constraint(), instance.get_has_restriction_value()):
159 print(f" constraint: {constraint.get_has_identifier()}, value: {value.get_has_value()}")
161 def _infer_property_type(self, instance) -> type:
162 """
163 Infers the concrete subtype (Relation, Attribute, Annotation) for a
164 generic Property instance (not better classified)
166 Strategy (in order):
167 1. Traverse UP the subPropertyOf chain: if any ancestor has a concrete
168 type, inherit it (if A subPropertyOf B and B is Relation, A is Relation).
169 2. Traverse DOWN by scanning the cache for properties that declare this
170 instance as their superproperty: if any subproperty has a concrete
171 type, inherit it (if B is Relation and B subPropertyOf A, A is Relation).
172 3. Fall back to Annotation if no type can be inferred from the hierarchy.
173 Annotation makes no domain/range assumptions and is valid for any
174 subject/object combination.
175 """
177 visited = set()
178 queue = [instance]
180 # (1) Goes up and down subproperty hierarchy
181 # (1.1) Traverse UP superproperties
182 while queue:
183 current = queue.pop(0)
184 if id(current) in visited:
185 continue
186 visited.add(id(current))
188 if type(current) in (Relation, Attribute, Annotation):
189 return type(current)
191 for sup in (current.get_is_sub_property_of() or []):
192 queue.append(sup)
194 # (1.2) Traverse DOWN subproperties
195 for instances_set in self._instance_cache.values():
196 for inst in instances_set:
197 if isinstance(inst, Property):
198 for sup in (inst.get_is_sub_property_of() or []):
199 if sup is instance:
200 t = self._infer_property_type(inst)
201 if t in (Relation, Attribute, Annotation):
202 return t
204 # (2) fallback
205 return Annotation
207 def _enrich_or_apply_owl_defaults(self, instance, uri):
208 """
209 Applies OWL-mandated defaults to instances lacking explicit declarations.
211 OWL open-world assumption requires every property to have a domain and
212 range. These are resolved first by traversing the rdfs:subPropertyOf
213 chain upward — if an ancestor declares domain/range, those are inherited.
214 Only if no value is found anywhere in the chain does the default apply.
216 Relation (owl:ObjectProperty):
217 domain -> owl:Thing (applies to any individual)
218 range -> owl:Thing (returns any individual)
220 Attribute (owl:DatatypeProperty):
221 domain -> owl:Thing (applies to any individual)
222 range -> rdfs:Literal (returns any literal value)
224 Individual:
225 If no rdf:type is declared, defaults to owl:Thing — the individual
226 exists but its class is unknown.
227 """
229 owl_thing = self.get_or_create(OWL.Thing, Concept)
230 rdfs_string = self.get_or_create(RDFS.Literal, Datatype)
232 if isinstance(instance, Relation):
233 inherited_domain = self._get_inherited_property_values(instance, "get_has_domain")
234 if len(inherited_domain) == 0:
235 instance.set_has_domain(owl_thing)
236 else:
237 for domain in inherited_domain:
238 instance.set_has_domain(domain)
240 inherited_range = self._get_inherited_property_values(instance, "get_has_range")
241 if len(inherited_range) == 0:
242 instance.set_has_range(owl_thing)
243 else:
244 for range in inherited_range:
245 instance.set_has_range(range)
247 if isinstance(instance, Attribute):
249 inherited_domain = self._get_inherited_property_values(instance, "get_has_domain")
250 if len(inherited_domain) == 0:
251 instance.set_has_domain(owl_thing)
252 else:
253 for domain in inherited_domain:
254 instance.set_has_domain(domain)
256 inherited_range = self._get_inherited_property_values(instance, "get_has_range")
257 if len(inherited_range) == 0:
258 instance.set_has_range(rdfs_string)
259 else:
260 for range in inherited_range:
261 instance.set_has_range(range)
263 # Default type per Individual
264 if isinstance(instance, Individual):
265 if not instance.get_has_type():
266 owl_thing = self.get_or_create(OWL.Thing, Concept)
267 instance.set_has_type(owl_thing)
270 if isinstance(instance, Resource):
271 # adds to Resources involved in punning the also defined as
272 if len(self._instance_cache[uri]) > 1:
273 for other in self._instance_cache[uri]:
274 if other is not instance:
275 instance.set_also_defined_as(other)
278 def _get_inherited_property_values(self, property_instance, getter_name: str) -> list:
279 """
280 Traversal upward along rdfs:subPropertyOf looking for values
281 exposed by getter_name (e.g. get_has_domain, get_has_range).
282 Returns list of values, or [] if none found in the chain.
283 """
284 def collect(node):
285 getter = getattr(node, getter_name, None)
286 if getter:
287 values = getter()
288 if values:
289 return values if isinstance(values, list) else [values]
290 return None
292 # (1) If this is a Relation with an inverse, check swapped domain/range on the inverse
293 if isinstance(property_instance, Relation):
294 inverse = property_instance.get_is_inverse_of()
295 if inverse:
296 swapped_getter = {
297 "get_has_domain": "get_has_range",
298 "get_has_range": "get_has_domain",
299 }.get(getter_name)
300 if swapped_getter:
301 swapped = getattr(inverse, swapped_getter, lambda: None)()
302 if swapped:
303 return swapped if isinstance(swapped, list) else [swapped]
305 # Traverses the hierarchy up to inherit domain and ranges
306 result = self._traverse_hierarchy(
307 property_instance,
308 next_getter="get_is_sub_property_of",
309 direction="up",
310 collect=collect,
311 )
312 return result if result is not None else []
315 def _infer_property_type(self, instance) -> type:
316 """
317 Infers the concrete subtype (Relation, Attribute, Annotation) for a
318 generic Property instance.
320 Strategy:
321 1. owl:inverseOf — if inverse is a Relation, this is a Relation.
322 2. Traverse UP subPropertyOf — inherit type from ancestor.
323 3. Traverse DOWN subPropertyOf — inherit type from descendant.
324 4. Fallback to Annotation.
325 """
326 # (1) inverseOf
327 inverse = getattr(instance, 'get_is_inverse_of', lambda: None)()
328 if inverse and type(inverse) is Relation:
329 return Relation
331 CONCRETE = (Relation, Attribute, Annotation)
333 # (2) UP THE HIERARCHY
334 def collect_type(node):
335 if type(node) in CONCRETE:
336 return type(node)
337 return None
339 result = self._traverse_hierarchy(
340 instance,
341 next_getter="get_is_sub_property_of",
342 direction="up",
343 collect=collect_type,
344 )
345 if result:
346 return result
348 # (3) DOWN THE HIERARCHY
349 result = self._traverse_hierarchy(
350 instance,
351 next_getter="get_is_sub_property_of",
352 direction="down",
353 collect=collect_type,
354 )
355 if result:
356 return result
358 # (4) fallback
359 return Annotation
361 # ========== HELPERS & RELATED FUNCTIONS PHASE 1 ==========
363 def _classify_nested(self, classified, predicates):
364 """
365 Recursively classifies BNodes found inside OWL list collections
366 (owl:intersectionOf, owl:unionOf, owl:oneOf) hanging off already-classified
367 BNodes. Extends the classified dict in place with any newly discovered
368 BNode-to-class mappings.
369 """
371 list_preds = [OWL.intersectionOf, OWL.unionOf, OWL.oneOf]
373 for bnode in list(classified.keys()):
374 for pred, obj in self.graph.predicate_objects(bnode):
375 if pred in list_preds:
376 try:
377 collection = RDFLibCollection(self.graph, obj)
378 for item in collection:
379 if isinstance(item, BNode) and item not in classified:
380 py_class = self._strategy.classify_by_predicate(item, self.graph)
381 if py_class:
382 classified[item] = py_class
383 except:
384 pass
386 # ========== HELPERS & RELATED FUNCTIONS PHASE 2 ==========
388 def _apply_setters_immediate(self, instance, setters_config):
389 """Applies static setter values from the config directly to an instance,
390 without resolving any RDF object. Used in phase2 to apply constant values
391 (e.g. set_is_symmetric: True) defined alongside is: class entries.
392 """
393 for setter_item in setters_config:
394 if isinstance(setter_item, dict):
395 for setter_name, value in setter_item.items():
396 if hasattr(instance, setter_name):
397 getattr(instance, setter_name)(value)
398 else:
399 if hasattr(instance, setter_item):
400 getattr(instance, setter_item)()
402 # ========== HANDLERS PHASE 4 (GROUP AXIOMS) ==========
404 def process_all_disjoint_classes(self, uri: Node):
405 """
406 Handles owl:AllDisjointClasses axioms by resolving the owl:members
407 collection and marking every pair of member Concepts as mutually
408 disjoint via set_is_disjoint_with.
409 """
410 members_list = self.graph.value(uri, OWL.members)
411 if not members_list:
412 return
413 try:
414 members = list(RDFLibCollection(self.graph, members_list))
415 for i, class_a_uri in enumerate(members):
416 class_a = self.get_or_create(class_a_uri, Concept)
417 for class_b_uri in members[i + 1:]:
418 class_b = self.get_or_create(class_b_uri, Concept)
419 class_a.set_is_disjoint_with(class_b)
420 class_b.set_is_disjoint_with(class_a)
421 except Exception as e:
422 print(f"Errore AllDisjointClasses: {e}")
424 def process_all_different(self, uri: Node):
425 """
426 Handles owl:AllDifferent axioms by resolving the owl:distinctMembers
427 collection and marking every pair of member Individuals as mutually
428 different via set_is_different_from.
429 """
430 members_list = self.graph.value(uri, OWL.distinctMembers)
431 if not members_list:
432 return
433 try:
434 members = list(RDFLibCollection(self.graph, members_list))
435 for i, ind_a_uri in enumerate(members):
436 ind_a = self.get_or_create(ind_a_uri, Individual)
437 for ind_b_uri in members[i + 1:]:
438 ind_b = self.get_or_create(ind_b_uri, Individual)
439 ind_a.set_is_different_from(ind_b)
440 ind_b.set_is_different_from(ind_a)
441 except Exception as e:
442 print(f"Errore AllDifferent: {e}")
444 def process_all_disjoint_properties(self, uri: Node):
445 """
446 Handles owl:AllDisjointProperties axioms by resolving the owl:members
447 collection and marking every pair of member Properties as mutually
448 disjoint via set_is_disjoint_with.
449 """
450 members_list = self.graph.value(uri, OWL.members)
451 if not members_list:
452 return
453 try:
454 members = list(RDFLibCollection(self.graph, members_list))
455 for i, prop_a_uri in enumerate(members):
456 prop_a = self.get_or_create(prop_a_uri, Property)
457 for prop_b_uri in members[i + 1:]:
458 prop_b = self.get_or_create(prop_b_uri, Property)
459 prop_a.set_is_disjoint_with(prop_b)
460 prop_b.set_is_disjoint_with(prop_a)
461 except Exception as e:
462 print(f"Errore AllDisjointProperties: {e}")
464 # ========== HANDLER CALLED BY PHASE 3 FOR POPULATING INSTANCES ==========
466 def handle_property_chain(self, instance, uri, predicate, obj, setter=None):
467 try:
468 collection = RDFLibCollection(self.graph, obj)
469 chain = [self.get_or_create(chain_uri, Relation) for chain_uri in collection]
470 instance.set_has_property_chain(chain)
471 except Exception as e:
472 print(f"Errore propertyChain: {e}")
474 # ========== RESTRICTIONS ================================================
476 def handle_datatype_restriction(self, instance, uri, predicate, obj, setter=None):
477 try:
478 collection = RDFLibCollection(self.graph, obj)
479 for facet_node in collection:
480 for facet_pred, facet_val in self.graph.predicate_objects(facet_node):
481 if isinstance(facet_val, RDFlibLiteral):
482 annotation = self.get_or_create(facet_pred, Annotation)
483 literal = self._create_literal(facet_val)
484 instance.set_has_constraint(annotation)
485 instance.set_has_restriction_value(literal)
486 except Exception as e:
487 print(f"Errore handle_datatype_restriction: {e}")
489 def handle_cardinality_exactly(self, instance, uri, predicate, obj, setter=None):
490 instance.set_has_cardinality_type("exactly")
491 instance.set_has_cardinality(obj)
492 instance.set_applies_on_concept(self.get_or_create(OWL.Thing, Concept))
494 def handle_cardinality_min(self, instance, uri, predicate, obj, setter=None):
495 instance.set_has_cardinality_type("min")
496 instance.set_has_cardinality(obj)
497 instance.set_applies_on_concept(self.get_or_create(OWL.Thing, Concept))
499 def handle_cardinality_max(self, instance, uri, predicate, obj, setter=None):
500 instance.set_has_cardinality_type("max")
501 instance.set_has_cardinality(obj)
502 instance.set_applies_on_concept(self.get_or_create(OWL.Thing, Concept))
504 # ========== HANDLER TRUTH FUNCTIONS ==========
506 def _build_truth_function(self, instance, obj, operator):
507 """
508 Se instance e' TruthFunction: popola in-place, ritorna None.
509 Se instance e' Concept: crea TruthFunction separata keyed su obj, ritorna tf.
510 """
511 if type(instance) is TruthFunction:
512 instance.set_has_logical_operator(operator)
513 try:
514 for item in RDFLibCollection(self.graph, obj):
515 concept = self.get_or_create(item, Concept)
516 if concept:
517 instance.set_applies_on_concept(concept)
518 except Exception as e:
519 print(f"Errore build_truth_function: {e}")
520 return None
521 else:
522 tf = self.get_or_create(obj, TruthFunction)
523 if tf:
524 tf.set_has_logical_operator(operator)
525 try:
526 for item in RDFLibCollection(self.graph, obj):
527 concept = self.get_or_create(item, Concept)
528 if concept:
529 tf.set_applies_on_concept(concept)
530 except Exception as e:
531 print(f"Errore build_truth_function: {e}")
532 return tf
534 # HANDLER FOR RELATION INVERSE OF, INFERS THE INVERSE - domains and ranges are inferred in phase 5
535 def handle_inverse_of(self, instance, uri, predicate, obj, setter=None):
537 instance = self.get_or_create(uri, Relation)
538 inverse = self.get_or_create(obj, Relation)
539 instance.set_is_inverse_of(inverse)
540 inverse.set_is_inverse_of(instance)
542 def handle_intersection(self, instance, uri, predicate, obj, setter=None):
543 if type(instance) not in (TruthFunction, Concept):
544 return
545 tf = self._build_truth_function(instance, obj, "and")
546 if tf and isinstance(instance, Concept):
547 instance.set_is_equivalent_to(tf)
549 def handle_union(self, instance, uri, predicate, obj, setter=None):
550 if type(instance) not in (TruthFunction, Concept):
551 return
552 tf = self._build_truth_function(instance, obj, "or")
553 if tf and isinstance(instance, Concept):
554 instance.set_is_equivalent_to(tf)
556 def handle_complement(self, instance, uri, predicate, obj, setter=None):
557 if type(instance) not in (TruthFunction, Concept):
558 return
559 if type(instance) is TruthFunction:
560 instance.set_has_logical_operator("not")
561 concept = self.get_or_create(obj, Concept)
562 if concept:
563 instance.set_applies_on_concept(concept)
564 else:
565 tf = self.get_or_create(obj, TruthFunction)
566 if tf:
567 tf.set_has_logical_operator("not")
568 concept = self.get_or_create(obj, Concept)
569 if concept:
570 tf.set_applies_on_concept(concept)
571 instance.set_is_equivalent_to(tf)
573 def handle_one_of(self, instance, uri, predicate, obj, setter=None):
574 if type(instance) not in (OneOf, Concept):
575 return
576 if isinstance(instance, Concept):
577 one_of = self.get_or_create(uri, OneOf)
578 if one_of:
579 try:
580 for item in RDFLibCollection(self.graph, obj):
581 resource = self.get_or_create(item, Individual)
582 if resource:
583 one_of.set_applies_on_resource(resource)
584 except Exception as e:
585 print(f"Errore oneOf su Concept: {e}")
586 instance.set_is_equivalent_to(one_of)
587 else:
588 try:
589 for item in RDFLibCollection(self.graph, obj):
590 resource = self.get_or_create(item, Individual)
591 if resource:
592 instance.set_applies_on_resource(resource)
593 except Exception as e:
594 print(f"Errore oneOf: {e}")
596 # ========== OVERRIDE Statement per OWL (typed subject/object) ==========
598 def _create_statement_for_triple(self, subj, pred, obj):
599 """OWL override: soggetto -> Individual, oggetto tipizzato."""
600 statement = Statement()
601 stmt_bnode = BNode()
602 statement.set_has_identifier(str(stmt_bnode))
604 if statement not in self._triples_map:
605 self._triples_map[statement] = set()
606 self._triples_map[statement].add((subj, pred, obj))
608 if pred != RDF.type:
609 # if the subject of a Statement is a BNode, then its reification (they can be shacl shapes :))
610 if isinstance(subj, BNode):
611 subj_inst = self.get_or_create(subj, Statement)
612 # otherwise its most likely a Named Individual (get_or_create decides the actual class)
613 else:
614 subj_inst = self.get_or_create(subj, Individual)
616 statement.set_has_subject(subj_inst)
618 if self._is_rdf_collection(obj):
619 obj_inst = self._convert_collection_to_container(obj)
620 pred_inst = self.get_or_create(pred, Relation)
621 elif isinstance(obj, RDFlibLiteral):
622 obj_inst = self._create_literal(obj)
623 # if the predicate is not in cache creates an annotation
624 if pred not in self._instance_cache:
625 pred_inst = self.get_or_create(pred, Annotation)
626 # otherwise reuses the existing predicate
627 else:
628 pred_inst = self.get_or_create(pred, Property)
629 elif isinstance(obj, BNode):
630 # BNode object = reified annotation, use existing instance or create new Statement
631 obj_inst = self.get_or_create(obj, Statement)
632 # if the predicate is not in cache creates an annotation
633 if pred not in self._instance_cache:
634 pred_inst = self.get_or_create(pred, Annotation)
635 # otherwise reuses the existing predicate
636 else:
637 pred_inst = self.get_or_create(pred, Property)
639 else:
640 obj_inst = self.get_or_create(obj, Resource)
641 # if the predicate is not in cache creates an annotation
642 if pred not in self._instance_cache:
643 pred_inst = self.get_or_create(pred, Annotation)
644 # otherwise reuses the existing predicate
645 else:
646 pred_inst = self.get_or_create(pred, Property)
648 # pred or obj may be None if their URI is in a protected namespace (e.g. rdf:, rdfs:)
649 if pred_inst is None:
650 pred_inst = Annotation()
651 pred_inst.set_has_identifier(str(pred))
653 if obj_inst is None:
654 obj_inst = Resource()
655 obj_inst.set_has_identifier(str(obj))
657 statement.set_has_predicate(pred_inst)
658 statement.set_has_object(obj_inst)
660 if stmt_bnode not in self._instance_cache:
661 self._instance_cache[stmt_bnode] = set()
662 self._instance_cache[stmt_bnode].add(statement)