Kastor Reasoning Framework

Design note (architecture, registry, onto-quality v0.4 integration): Reasoning in Kastor.

The Kastor RDF framework now includes a comprehensive reasoning system that provides pluggable reasoning capabilities through a provider mechanism, similar to the existing RDF provider architecture.

πŸ—οΈ Architecture Overview

Core Components

  1. RdfReasonerProvider - Interface for reasoner providers
  2. RdfReasoner - Core reasoner interface
  3. ReasonerConfig - Configuration for reasoning operations
  4. ReasonerRegistry - Central registry for discovering reasoners
  5. ReasoningResult - Comprehensive reasoning results

Supported Reasoner Types

  • RDFS - RDF Schema reasoning
  • OWL-EL - OWL 2 EL profile reasoning
  • OWL-RL - OWL 2 RL profile reasoning
  • OWL-DL - Full OWL 2 DL reasoning
  • Custom - Custom rule-based reasoning

Which types are actually served depends on the providers on the classpath:

Provider Supported ReasonerTypes Notes
Memory (built in) RDFS RDFS rules rdfs2, rdfs3, rdfs5, rdfs7, rdfs9 and rdfs11 applied to a fixpoint; no axiomatic triples
Jena (rdf-jena-reasoning) RDFS, OWL_MICRO, OWL_RL, CUSTOM OWL_MICRO uses Jena’s OWL Micro rule reasoner (fast, incomplete OWL fragment); OWL_RL uses Jena’s OWL rule reasoner, which is close to but not a complete OWL 2 RL implementation; CUSTOM requires at least one customRules entry; OWL_EL, OWL_QL, OWL_DL are rejected
RDF4J (rdf-rdf4j-reasoning) RDFS RDF4J ships no OWL reasoner
HermiT (rdf-reasoning-hermit) HERMIT, OWL_DL OWL 2 DL; defaults to streamingMode = false; the timeout is enforced by a watchdog that interrupts the reasoner; owl:imports is never followed (see below)

Consistency results come from the engine’s own checks rather than being assumed.

HermiT never resolves owl:imports. The graph you pass is the whole input: an owl:imports triple in it is kept as a declaration, but its target is not fetched over HTTP, not read from a file: IRI, and contributes nothing to the result; an import that could not be resolved anyway does not fail the call. Reasoning over an imported ontology means merging it into the graph first, loaded under your own policy (for example Rdf.parseFromUrl(url, format, UrlLoadOptions(...))).

Provider selection is deterministic. When several providers support a type, the one with the highest priority() wins: HermiT 100, Jena 50, RDF4J 40, memory βˆ’100. RdfReasoning.reasoner(type) uses ReasonerConfig.forType(type), which picks the recommended settings for that type (for example, hermit() / owlDl() for HERMIT / OWL_DL).

πŸ“¦ Module Structure

rdf/
β”œβ”€β”€ core/                        # Core RDF interfaces (no reasoning)
β”œβ”€β”€ reasoning/
β”‚   β”œβ”€β”€ facade/                  # rdf-reasoning: RdfReasoning, RdfReasonerProvider/RdfReasoner,
β”‚   β”‚                            #   ReasonerConfig, ReasonerRegistry, ReasoningResults,
β”‚   β”‚                            #   providers/MemoryReasonerProvider
β”‚   └── hermit/                  # rdf-reasoning-hermit: HermitReasonerProvider (OWL 2 DL)
β”œβ”€β”€ providers/
β”‚   β”œβ”€β”€ jena-reasoning/          # rdf-jena-reasoning: JenaReasonerProvider (+ SPI)
β”‚   └── rdf4j-reasoning/         # rdf-rdf4j-reasoning: Rdf4jReasonerProvider (+ SPI)
└── examples/                    # BasicReasoningExample.kt, ReasonerProviderExample.kt

πŸš€ Usage Examples

Basic Reasoning

import com.geoknoesis.kastor.rdf.*
import com.geoknoesis.kastor.rdf.reasoning.*
import com.geoknoesis.kastor.rdf.vocab.RDF
import com.geoknoesis.kastor.rdf.vocab.RDFS

// Create a graph with RDFS schema
val graph = Rdf.graph {
    prefixes {
        put("ex", "http://example.org/")
    }
    
    // Define class hierarchy
    val person = iri("ex:Person")
    val student = iri("ex:Student")
    val teacher = iri("ex:Teacher")
    
    person - RDFS.subClassOf - RDFS.Resource
    student - RDFS.subClassOf - person
    teacher - RDFS.subClassOf - person
    
    // Define property hierarchy
    val knows = iri("ex:knows")
    val teaches = iri("ex:teaches")
    
    knows - RDFS.subPropertyOf - RDFS.seeAlso
    teaches - RDFS.subPropertyOf - knows
    
    // Domain and range
    knows - RDFS.domain - person
    knows - RDFS.range - person
    
    // Instances
    val alice = iri("ex:alice")
    val bob = iri("ex:bob")
    
    alice - RDF.type - student
    bob - RDF.type - teacher
    alice - teaches - bob
}

// Create a reasoner
val reasoner = RdfReasoning.reasoner(ReasonerType.RDFS)

// Perform reasoning
val result = reasoner.reason(graph)

println("Found ${result.inferredTriples.size} inferred triples")
println("Graph is consistent: ${result.consistencyCheck.isConsistent}")

// Show inferred triples
result.inferredTriples.forEach { triple ->
    println("  $triple")
}

Different Reasoner Providers

// Get available providers
val providers = RdfReasoning.reasonerProviders()
providers.forEach { provider ->
    println("${provider.name} (${provider.getType()}) - v${provider.version}")
    println("  Supported types: ${provider.getSupportedTypes().joinToString(", ")}")
}

// Use specific provider
// (com.geoknoesis.kastor.rdf.jena.reasoning.JenaReasonerProvider,
//  com.geoknoesis.kastor.rdf.rdf4j.reasoning.Rdf4jReasonerProvider)
val jenaReasoner = JenaReasonerProvider().createReasoner(ReasonerConfig.rdfs())
val rdf4jReasoner = Rdf4jReasonerProvider().createReasoner(ReasonerConfig.rdfs())

Configuration Options

// Default configuration
val defaultConfig = ReasonerConfig.default()

// RDFS-specific configuration
val rdfsConfig = ReasonerConfig.rdfs()

// OWL-EL configuration (no bundled provider serves OWL_EL; Jena rejects it)
val owlElConfig = ReasonerConfig.owlEl()

// Recommended configuration for a type (used by RdfReasoning.reasoner(type))
val dlConfig = ReasonerConfig.forType(ReasonerType.OWL_DL)

// Large graph configuration
val largeGraphConfig = ReasonerConfig.forLargeGraphs()

// Memory-constrained configuration
val memoryConfig = ReasonerConfig.forMemoryConstrained()

// Custom configuration
val customConfig = ReasonerConfig(
    reasonerType = ReasonerType.RDFS,
    enabledRules = setOf(
        ReasoningRule.RDFS_SUBCLASS,
        ReasoningRule.RDFS_SUBPROPERTY
    ),
    timeout = Duration.ofMinutes(10),
    includeAxioms = true
)

πŸ”§ Reasoning Results

The ReasoningResult provides comprehensive information about the reasoning process:

data class ReasoningResult(
    val originalGraph: RdfGraph,
    val inferredTriples: List<RdfTriple>,
    val classification: ClassificationResult?,
    val consistencyCheck: ConsistencyResult,
    val reasoningTime: Duration,
    val statistics: ReasoningStatistics
)

Classification Results

data class ClassificationResult(
    val classHierarchy: Map<Iri, List<Iri>>,      // class -> superclasses
    val instanceClassifications: Map<Iri, List<Iri>>, // instance -> types
    val propertyHierarchy: Map<Iri, List<Iri>>,   // property -> superproperties
    val equivalentClasses: Map<Iri, Set<Iri>>,
    val disjointClasses: Map<Iri, Set<Iri>>
)

Consistency Checking

data class ConsistencyResult(
    val isConsistent: Boolean,
    val inconsistencies: List<Inconsistency>,
    val warnings: List<String>,
    val explanations: List<Explanation>
)

Validation Reports

RdfReasoner.validateOntology(graph) returns the reasoning module’s own com.geoknoesis.kastor.rdf.reasoning.ValidationReport (not the SHACL ValidationReport):

data class ValidationReport(
    val isValid: Boolean,
    val violations: List<ValidationViolation>,
    val warnings: List<String>,
    val statistics: ValidationStatistics
)

🎯 Provider Capabilities

Each reasoner provider reports its capabilities:

data class ReasonerCapabilities(
    val supportedTypes: Set<ReasonerType>,
    val supportsIncrementalReasoning: Boolean,
    val supportsCustomRules: Boolean,
    val supportsExplanation: Boolean,
    val supportsConsistencyChecking: Boolean,
    val supportsClassification: Boolean,
    val maxGraphSize: Long,
    val typicalPerformance: PerformanceProfile
)

πŸ”Œ Service Provider Discovery

The framework uses Java ServiceLoader for automatic discovery of reasoner providers:

  • rdf-reasoning artifact: MemoryReasonerProvider (always available once rdf-reasoning is on the classpath)
  • rdf-jena-reasoning artifact: JenaReasonerProvider (SPI + direct import)
  • rdf-rdf4j-reasoning artifact: Rdf4jReasonerProvider (SPI + direct import)

Add com.geoknoesis.kastor:rdf-jena-reasoning / com.geoknoesis.kastor:rdf-rdf4j-reasoning alongside com.geoknoesis.kastor:rdf-jena / rdf-rdf4j when you need those providers; they are not transitive from the store adapters.

πŸ“Š Performance Considerations

Each provider reports a coarse typicalPerformance hint in ReasonerCapabilities:

Provider typicalPerformance Supported types Custom rules
Memory (MemoryReasonerProvider) FAST RDFS No
Jena (JenaReasonerProvider) MEDIUM RDFS, OWL_MICRO, OWL_RL, CUSTOM Yes
RDF4J (Rdf4jReasonerProvider) FAST RDFS No
HermiT (HermitReasonerProvider) SLOW HERMIT, OWL_DL No

These hints are not benchmarks, and none of the providers supports incremental reasoning. Measure with your own data before choosing a backend.

πŸ§ͺ Testing

Comprehensive tests are provided:

class BasicReasoningTest {
    @Test
    fun `memory reasoner performs basic RDFS inference`() {
        val graph = createSampleGraph()
        // Picks the highest-priority RDFS provider: the memory reasoner unless Jena/RDF4J reasoning is on the classpath
        val reasoner = RdfReasoning.reasoner(ReasonerType.RDFS)
        
        val result = reasoner.reason(graph)
        
        assertNotNull(result)
        assertTrue(result.inferredTriples.isNotEmpty())
        assertTrue(result.consistencyCheck.isConsistent)
        assertNotNull(result.classification)
    }
}

πŸš€ Future Enhancements

The architecture is designed to support:

  1. Advanced Reasoners: Pellet, FaCT++ (HermiT is already available via rdf-reasoning-hermit)
  2. Streaming Reasoning: For very large graphs
  3. Incremental Reasoning: Only reason about new/changed triples
  4. Custom Rules on more backends: CUSTOM rules are currently supported by the Jena provider only
  5. Explanation Generation: Why certain triples were inferred
  6. Performance Optimization: Caching, parallel processing

πŸ“ Migration Guide

To add reasoning to existing Kastor applications:

  1. Add Dependencies:
    implementation(project(":rdf:core"))
    implementation(project(":rdf:reasoning"))
    implementation(project(":rdf:jena"))
    implementation(project(":rdf:jena-reasoning"))   // Jena-backed RdfReasonerProvider
    implementation(project(":rdf:rdf4j"))
    implementation(project(":rdf:rdf4j-reasoning")) // RDF4J-backed RdfReasonerProvider
    

    For Maven coordinates, use the kastor-bom; optional reasoners are published as rdf-jena-reasoning and rdf-rdf4j-reasoning.

  2. Use Reasoning:
    val reasoner = RdfReasoning.reasoner(ReasonerType.RDFS)
    val result = reasoner.reason(yourGraph)
    
  3. Handle Results:
    // Add inferred triples to your repository
    repository.addTriples(null, result.inferredTriples)
       
    // Check consistency
    if (!result.consistencyCheck.isConsistent) {
        // Handle inconsistencies
    }
    

The reasoning framework integrates seamlessly with the existing Kastor RDF API, providing powerful inference capabilities while maintaining the same clean, DSL-based approach that developers already know and love.