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
RdfReasonerProvider- Interface for reasoner providersRdfReasoner- Core reasoner interfaceReasonerConfig- Configuration for reasoning operationsReasonerRegistry- Central registry for discovering reasonersReasoningResult- 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-reasoningartifact:MemoryReasonerProvider(always available oncerdf-reasoningis on the classpath)rdf-jena-reasoningartifact:JenaReasonerProvider(SPI + direct import)rdf-rdf4j-reasoningartifact: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:
- Advanced Reasoners: Pellet, FaCT++ (HermiT is already available via
rdf-reasoning-hermit) - Streaming Reasoning: For very large graphs
- Incremental Reasoning: Only reason about new/changed triples
- Custom Rules on more backends:
CUSTOMrules are currently supported by the Jena provider only - Explanation Generation: Why certain triples were inferred
- Performance Optimization: Caching, parallel processing
π Migration Guide
To add reasoning to existing Kastor applications:
- 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 RdfReasonerProviderFor Maven coordinates, use the kastor-bom; optional reasoners are published as
rdf-jena-reasoningandrdf-rdf4j-reasoning. - Use Reasoning:
val reasoner = RdfReasoning.reasoner(ReasonerType.RDFS) val result = reasoner.reason(yourGraph) - 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.