Skip to content

ontocast.onto.graph_prune

Seed-free graph pruning shared by induced-subgraph retrieval and prompt condensing.

These pruners and predicate vocabularies were written for the vector-retrieval induced-subgraph builder in :mod:ontocast.tool.sparql, which is the only place in the pipeline that ever bounded how much ontology reached the LLM. The prompt condenser (:mod:ontocast.onto.ontology_condense) needs the same judgements about which triples carry the schema and which are scaffolding, so they live here rather than being duplicated: two copies of "what is safe to drop" would drift, and the drift would be invisible until an extraction quietly lost a term.

Everything here operates on a materialized graph with no seed list, no relevance scores and no triple store, which is what makes it reusable outside retrieval.

Attributes

BFS_PREDICATE_PRIORITY = (frozenset({RDFS.label, SKOS.prefLabel}), frozenset({RDF.type}), frozenset({RDFS.subClassOf, OWL.equivalentClass}), frozenset({RDFS.domain, RDFS.range, RDFS.subPropertyOf}), frozenset({RDFS.comment, SKOS.definition, SKOS.scopeNote, SKOS.altLabel})) module-attribute

GENERIC_INDIVIDUAL_TYPES = frozenset({OWL.NamedIndividual, OWL.Class, RDFS.Class}) module-attribute

MIN_MEANINGFUL_RESTRICTION_PREDICATES = 2 module-attribute

NOISY_EXPANSION_PREDICATES = frozenset({RDF.first, RDF.rest, OWL.imports, OWL.versionIRI, OWL.versionInfo, OWL.priorVersion, OWL.backwardCompatibleWith, OWL.incompatibleWith, DCTERMS.creator, DCTERMS.license, DCTERMS.created, DCTERMS.modified, DCTERMS.identifier, DCTERMS.publisher, DCTERMS.contributor}) module-attribute

OWL_RESTRICTION_MEANINGFUL_PREDICATES = frozenset({OWL.onProperty, OWL.someValuesFrom, OWL.allValuesFrom, OWL.hasValue, OWL.cardinality, OWL.minCardinality, OWL.maxCardinality, OWL.qualifiedCardinality, OWL.minQualifiedCardinality, OWL.maxQualifiedCardinality, OWL.onDataRange, OWL.onClass, OWL.oneOf}) module-attribute

Functions:

bfs_triple_rank(triple)

Sort key admitting defining triples before incidental ones, ties lexicographic.

Source code in ontocast/onto/graph_prune.py
def bfs_triple_rank(triple: tuple) -> tuple[int, str]:
    """Sort key admitting defining triples before incidental ones, ties lexicographic."""
    predicate = triple[1]
    for rank, predicates in enumerate(BFS_PREDICATE_PRIORITY):
        if predicate in predicates:
            return (rank, str(triple))
    return (len(BFS_PREDICATE_PRIORITY), str(triple))

count_meaningful_restriction_predicates(graph, bnode)

How many predicates of a restriction bnode actually constrain anything.

Source code in ontocast/onto/graph_prune.py
def count_meaningful_restriction_predicates(graph: Graph, bnode: BNode) -> int:
    """How many predicates of a restriction bnode actually constrain anything."""
    return sum(
        1
        for _, pred, _ in graph.triples((bnode, None, None))
        if pred in OWL_RESTRICTION_MEANINGFUL_PREDICATES
    )

prune_degenerate_restriction_bnodes(result)

Remove stub restriction blank nodes and subClassOf edges pointing to them.

Source code in ontocast/onto/graph_prune.py
def prune_degenerate_restriction_bnodes(result: Graph) -> int:
    """Remove stub restriction blank nodes and subClassOf edges pointing to them."""
    dropped = 0
    bnode_objects = sorted(
        {
            obj
            for _, _, obj in result.triples((None, RDFS.subClassOf, None))
            if isinstance(obj, BNode)
        },
        key=str,
    )
    for bnode in bnode_objects:
        if (
            count_meaningful_restriction_predicates(result, bnode)
            >= MIN_MEANINGFUL_RESTRICTION_PREDICATES
        ):
            continue
        remove_subclassof_to_bnode(result, bnode)
        remove_bnode_subgraph(result, bnode)
        dropped += 1
    return dropped

prune_orphaned_bnode_subjects(graph)

Remove blank-node subjects that no triple in the graph references as object.

Source code in ontocast/onto/graph_prune.py
def prune_orphaned_bnode_subjects(graph: Graph) -> None:
    """Remove blank-node subjects that no triple in the graph references as object."""
    bnode_as_object: set[BNode] = {o for _, _, o in graph if isinstance(o, BNode)}
    for triple in list(graph):
        subj, _, _ = triple
        if isinstance(subj, BNode) and subj not in bnode_as_object:
            graph.remove(triple)

remove_bnode_subgraph(graph, bnode)

Remove every triple asserted about a blank node.

Source code in ontocast/onto/graph_prune.py
def remove_bnode_subgraph(graph: Graph, bnode: BNode) -> None:
    """Remove every triple asserted about a blank node."""
    for triple in list(graph.triples((bnode, None, None))):
        graph.remove(triple)

remove_subclassof_to_bnode(graph, bnode)

Remove the class-axiom edges pointing at a blank node.

Source code in ontocast/onto/graph_prune.py
def remove_subclassof_to_bnode(graph: Graph, bnode: BNode) -> None:
    """Remove the class-axiom edges pointing at a blank node."""
    for triple in list(graph.triples((None, RDFS.subClassOf, bnode))):
        graph.remove(triple)
    for triple in list(graph.triples((None, OWL.equivalentClass, bnode))):
        graph.remove(triple)

strip_redundant_generic_types(graph)

Drop generic rdf:types when the subject has informative types or URI hierarchy.

Source code in ontocast/onto/graph_prune.py
def strip_redundant_generic_types(graph: Graph) -> None:
    """Drop generic rdf:types when the subject has informative types or URI hierarchy."""
    for subj, pred, obj in list(graph):
        if pred != RDF.type or obj not in GENERIC_INDIVIDUAL_TYPES:
            continue
        other_types = [
            term
            for _, _, term in graph.triples((subj, RDF.type, None))
            if term not in GENERIC_INDIVIDUAL_TYPES
        ]
        has_subclass_uri = any(
            isinstance(parent, URIRef)
            for _, _, parent in graph.triples((subj, RDFS.subClassOf, None))
        )
        if other_types or has_subclass_uri:
            graph.remove((subj, pred, obj))