Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ jobs:
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Install uv
uses: astral-sh/setup-uv@v2
uses: astral-sh/setup-uv@v7

- name: Install dependencies
run: uv sync
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,4 +362,6 @@ The `example_projects` folder contains contrived examples, but this has also bee
1. check and raise error or at least warning if default_union is set in underlying Dataset of DatasetView
1. document and/or fix serialisation: canon longTurtle is not great with the way it orders things, so we might need to call out to riot unfortunately.
1. consider changing the distinction from interal/external to data/vocabulary (where vocab includes taxonomies or ontologies) - basically the ABox/TBox distinction where CBox is part of TBox.
1. add support for ASK query
1. add better output support for ASK query
1. add option to remove project name from named graphs, for easier specification:
1. e.g. `<urn:pythinfer:inferences:owl>` which is easy to remember and specify on command-line.
18 changes: 0 additions & 18 deletions example_projects/eg0-basic/derived/expected_merged.trig

This file was deleted.

30 changes: 30 additions & 0 deletions example_projects/eg0-basic/expected/expected-0-merged.trig
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@prefix : <http://example.org/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix pythinfer: <http://pythinfer.local/> .

@base <http://pythinfer.local/eg0-basic/> .

<file/basic-data.ttl> {
:Bob a foaf:Person ;
foaf:knows :Alice ;
foaf:name "Bob Jones" .

:Alice a foaf:Person ;
foaf:age 30 ;
foaf:name "Alice Smith" .
}

<file/basic-model.ttl> {
foaf:knows a owl:SymmetricProperty .
}

<provenance> {
<file/basic-data.ttl> a pythinfer:SourceGraph ;
dcterms:source <file:///$PROJ_FOLDER/pythinfer/example_projects/eg0-basic/basic-data.ttl> ;
.
<file/basic-model.ttl> a pythinfer:SourceGraph ;
dcterms:source <file:///$PROJ_FOLDER/pythinfer/example_projects/eg0-basic/basic-model.ttl> ;
.
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@prefix : <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .

<inferences_owl> {
<http://pythinfer.local/eg0-basic/inferences/owl> {
:Alice foaf:knows :Bob .
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix pythinfer: <http://pythinfer.local/> .

<ancestors-model.ttl> {
@base <http://pythinfer.local/eg1-ancestors/> .

<file/ancestors-model.ttl> {
ex:childOf rdfs:label "child of" ;
owl:inverseOf ex:parentOf .

Expand Down Expand Up @@ -33,7 +36,7 @@
rdfs:label "Person" .
}

<ancestors-data.ttl> {
<file/ancestors-data.ttl> {
ex:Alice a ex:Person ;
ex:parentOf ex:Bob,
ex:Carol .
Expand All @@ -48,3 +51,9 @@

ex:Eve a ex:Person .
}

<provenance> {
<file/ancestors-data.ttl> a pythinfer:SourceGraph .
<file/ancestors-model.ttl> a pythinfer:SourceGraph .
<file/../../vocabs-external/skos.ttl> a pythinfer:SourceGraph .
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@prefix ex: <http://example.org/ancestor/> .

<inferences_owl> {
@base <http://pythinfer.local/eg1-ancestors/> .
<inferences/owl> {
ex:David ex:childOf ex:Bob ;
ex:descendantOf ex:Alice,
ex:Bob .
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
@prefix ptp: <http://www.example.org/pythinfer/project/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<inferences_sparql> {
@base <http://pythinfer.local/eg2-projects/> .

<inferences/sparql> {
# This is the most important SPARQL-based inference: the relationship between projA and projB
eg:relationship-projA-projB a ptp:ProjectRelationship ;
ptp:hasParticipant eg:projA,
Expand Down Expand Up @@ -47,7 +49,7 @@
}


<inferences_owl> {
<inferences/owl> {
eg:projA a dcat:Catalog,
prov:Activity ;
ptp:hasDataSource _:b0,
Expand Down
55 changes: 43 additions & 12 deletions src/pythinfer/infer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from owlrl import DeductiveClosure
from owlrl.OWLRL import OWLRL_Semantics
from rdflib import (
DCTERMS,
OWL,
RDF,
RDFS,
Expand All @@ -30,11 +31,9 @@
export_dataset,
load_sparql_inference_queries,
)
from pythinfer.inout import PYTHINFER_NS
from pythinfer.rdflibplus import DatasetView

IRI_EXTERNAL_INFERENCES: URIRef = URIRef("inferences_external") # type: ignore[bad-assignment]
IRI_OWL_INFERENCES: URIRef = URIRef("inferences_owl") # type: ignore[bad-assignment]
IRI_SPARQL_INFERENCES: URIRef = URIRef("inferences_sparql") # type: ignore[bad-assignment]

MAX_REASONING_ROUNDS = 5
SCRIPT_DIR = Path(__file__).parent
Expand Down Expand Up @@ -259,7 +258,7 @@ def filter_triples(


def _generate_external_inferences(
ds: Dataset, external_graph_ids: list[IdentifiedNode]
ds: Dataset, external_graph_ids: list[IdentifiedNode], project: Project
) -> Graph:
"""Generate inferences from external vocabularies only (step 2).

Expand All @@ -269,6 +268,7 @@ def _generate_external_inferences(
Args:
ds: Dataset containing all graphs.
external_graph_ids: List of graph identifiers that are external.
project: The project configuration.

Returns:
Graph containing external inferences.
Expand All @@ -286,13 +286,21 @@ def _generate_external_inferences(
info(" Temporary dataset created with %d triples in default graph", len(temp_ds))

# Create inferences graph in temp dataset (must share same store)
temp_inferences = temp_ds.graph(IRI_EXTERNAL_INFERENCES)
iri_external = project.inference_gid("external")
temp_inferences = temp_ds.graph(iri_external)
g_provenance = ds.graph(project.provenance_gid)

apply_owlrl_inference(temp_ds, temp_inferences)

g_external_inferences = ds.graph(IRI_EXTERNAL_INFERENCES)
g_external_inferences = ds.graph(iri_external)
for s, p, o in temp_inferences:
g_external_inferences.add((s, p, o))

# Add provenance metadata for external inference graph
g_provenance.add((iri_external, RDF.type, PYTHINFER_NS["InferenceGraph"]))
g_provenance.add(
(iri_external, PYTHINFER_NS["inferenceEngine"], Literal("owlrl"))
)
info(" External inferences generated: %d triples", len(g_external_inferences))
return g_external_inferences

Expand Down Expand Up @@ -406,16 +414,38 @@ def run_inference_backend(
sparql_queries = load_sparql_inference_queries(project.paths_sparql_inference or [])

# Step 2: Generate external inferences (once - this is the "noise floor")
g_external_inferences = _generate_external_inferences(ds, external_graph_ids)
g_external_inferences = _generate_external_inferences(
ds, external_graph_ids, project
)

# Steps 3-5: Iterate full inferences + heuristics until convergence
info(
"Steps 3-5: Iterating full inferences + heuristics (max %d iterations)...",
MAX_REASONING_ROUNDS,
)

g_inferences_owl = ds.graph(IRI_OWL_INFERENCES)
g_inferences_sparql = ds.graph(IRI_SPARQL_INFERENCES)
iri_owl = project.inference_gid("owl")
iri_sparql = project.inference_gid("sparql")
g_inferences_owl = ds.graph(iri_owl)
g_inferences_sparql = ds.graph(iri_sparql)
g_provenance = ds.graph(project.provenance_gid)

# Add provenance metadata for inference graphs
g_provenance.add((iri_owl, RDF.type, PYTHINFER_NS["InferenceGraph"]))
g_provenance.add(
(iri_owl, PYTHINFER_NS["inferenceEngine"], Literal(project.owl_backend))
)

g_provenance.add(
(iri_sparql, RDF.type, PYTHINFER_NS["InferenceGraph"])
)
g_provenance.add(
(
iri_sparql,
PYTHINFER_NS["inferenceEngine"],
Literal("SPARQL CONSTRUCT"),
)
)
iteration = 0
previous_triple_count = len(ds) # Count triples in entire dataset

Expand Down Expand Up @@ -491,18 +521,19 @@ def run_inference_backend(
len(g_inferences_owl) + len(g_inferences_sparql),
)

iri_external = project.inference_gid("external")
all_external_ids: list[IdentifiedNode] = [
*external_graph_ids,
IRI_EXTERNAL_INFERENCES,
iri_external,
]

output_file = output or project.path_output / f"{INFERRED_WANTED_FILESTEM}.trig"
output_file.parent.mkdir(parents=True, exist_ok=True)

output_ds = DatasetView(
ds,
[IRI_OWL_INFERENCES, IRI_SPARQL_INFERENCES]
+ ([IRI_EXTERNAL_INFERENCES] if export_external_inferences else []),
[iri_owl, iri_sparql]
+ ([iri_external] if export_external_inferences else []),
)

export_dataset(
Expand Down
Loading