Reasonable

April 24, 2026 · View on GitHub

Nightly Build Build PyPI version

An OWL 2 RL reasoner with reasonable performance

Performance

Comparing performance of reasonable with OWLRL and Allegro. Evaluation consisted of loading Brick models of different sizes into the respective reasoning engine and timing how long it took to produce the materialization. reasonable is about 7x faster than Allegro and 38x faster than OWLRL on this workload.

benchmark

How to Use

Command Line Interface

You can download a static build of the command line tool here (this is automatically built from the latest master).

Usage as follows:

$ reasonable -h
An OWL 2 RL reasoner with reasonable performance

Usage: reasonable [OPTIONS] <INPUT_FILES>...

Arguments:
  <INPUT_FILES>...  

Options:
  -o, --output-file <OUTPUT_FILE>  [default: output.ttl]
  -h, --help                       Print help
  -V, --version                    Print version

Example:

$ reasonable example_models/ontologies/Brick.n3 example_models/small1.n3 -o myout.ttl
[2023-07-04T15:31:52Z INFO  reasonable] Loading file example_models/ontologies/Brick.n3
[2023-07-04T15:31:52Z INFO  reasonable::reasoner] Loaded 14803 triples from file example_models/ontologies/Brick.n3
[2023-07-04T15:31:52Z INFO  reasonable] Loading file example_models/small1.n3
[2023-07-04T15:31:52Z INFO  reasonable::reasoner] Loaded 14 triples from file example_models/small1.n3
[2023-07-04T15:31:52Z INFO  reasonable] Starting reasoning
[2023-07-04T15:32:11Z INFO  reasonable] Reasoning completed in 18.66sec
[2023-07-04T15:32:11Z INFO  reasonable] Writing to myout.ttl

Diagnostics & CLI Flags

The reasoner records OWL 2 RL rule violations as diagnostics (not errors in the log). Use the CLI to view or enforce them:

  • Reporting format: --error-format text|json|ndjson (default: text)
  • Fail the run on rules: --fail-on cax-dw,prp-pdw (repeat or comma-separated; codes like OWLRL.CAX_DW also supported)
  • Limit output: --max-diagnostics N; summary only: --summary-only

Common diagnostic codes:

  • OWLRL.CAX_DW: Individual typed as two disjoint classes
  • OWLRL.PRP_PDW: Pair violates propertyDisjointWith
  • OWLRL.PRP_ASYP: Asymmetric property used both directions
  • OWLRL.PRP_IRP: Irreflexive property used with same subject/object
  • OWLRL.CLS_NOTHING: Individual typed as owl:Nothing

Example strict run:

$ reasonable model.ttl --error-format json --fail-on cax-dw,prp-pdw -o out.ttl

Python

To facilitate usage, we use the pyo3 project to generate Python 3.x bindings to this project. Installing these should be as easy as pip install reasonable.

See also the brickschema package for working with Brick models. The package provides a generic interface to this reasoner and several others.

Usage looks like:

import reasonable

# import triples from an rdflib Graph
import rdflib
g = rdflib.Graph()
g.parse("example_models/ontologies/Brick.n3", format="n3")
g.parse("example_models/small1.n3", format="n3")

r = reasonable.PyReasoner()
r.from_graph(g)
triples = r.reason()
print("from rdflib:", len(triples))

# import triples from files on disk
r = reasonable.PyReasoner()
r.load_file("example_models/ontologies/Brick.n3")
r.load_file("example_models/small1.n3")
triples = r.reason()
print("from files:", len(triples))

Incremental Reasoning

After the first reason() call, the reasoner supports incremental materialization. Use update_graph() to replace the base triples when your graph changes — the reasoner automatically diffs and selects incremental (additions only) or full re-materialization (if removals detected):

r = reasonable.PyReasoner()
r.from_graph(ontology + data)
r.reason()                        # full materialization

# data changes over time...
data.add(new_triple)
data.remove(old_triple)
r.update_graph(ontology + data)   # replaces base, auto-detects diff
r.reason()                        # incremental or full as needed

See the Python README for the full API reference.

Rust

See Rust docs

Example of usage from Rust:

use ::reasonable::owl::Reasoner;
use std::env;
use std::time::Instant;
use log::info;

fn main() {
    env_logger::init();
    let mut r = Reasoner::new();
    env::args().skip(1).map(|filename| {
        info!("Loading file {}", &filename);
        r.load_file(&filename).unwrap()
    }).count();
    let reasoning_start = Instant::now();
    info!("Starting reasoning");
    r.reason();
    info!("Reasoning completed in {:.02}sec", reasoning_start.elapsed().as_secs_f64());
    r.dump_file("output.ttl").unwrap();
}

Building from Source

Most common workflows are wrapped as make targets. You'll need a Rust toolchain (install via rustup) for everything, and additionally uv plus Python 3.9+ for the Python bindings.

Rust library and CLI

make build        # release build; produces ./target/release/reasonable
make test         # run the Rust test suite
make bench        # end-to-end benchmark vs. OWLRL on example_models/

For the criterion micro-benchmarks in benches/my_benchmark.rs, run cargo bench directly.

Python bindings

The Python bindings live in python/ and are built with maturin via uv.

make dev-python-library   # build the extension and install it into python/.venv
make test-python          # build + run the pytest suite in python/tests/
make bench-python         # run benches/python/bench.py (vs. OWLRL, Allegro, owlready2)
make build-python-library # build a distributable wheel into python/dist/

OWL 2 Rules

Using rule definitions from here.

TODO: implement remaining RDF/RDFS entailment semantics as described here

RDFS Semantics

CompletedRule nameNotes
yesrdfs11rdfs:subClassOf transitivity

Note: haven't implemented rules that produce exceptions; waiting to determine the best way of handling these errors.

Equality Semantics

CompletedRule nameNotes
noeq-refimplementation is very inefficient; causes lots of flux
yeseq-sym
yeseq-trans
yeseq-rep-s
yeseq-rep-p
yeseq-rep-o
noeq-diff1throws exception
noeq-diff2throws exception
noeq-diff3throws exception

Property Axiom Semantics

CompletedRule nameNotes
noprp-ap
yesprp-dom
yesprp-rng
yesprp-fp
yesprp-ifp
yesprp-irpthrows exception
yesprp-symp
yesprp-asypthrows exception
yesprp-trp
yesprp-spo1
noprp-spo2
yesprp-eqp1
yesprp-eqp2
yesprp-pdwthrows exception
noprp-adpthrows exception
yesprp-inv1
yesprp-inv2
noprp-key
noprp-npa1throws exception
noprp-npa2throws exception

Class Semantics

CompletedRule nameNotes
yescls-thing
yescls-nothing1
yescls-nothing2throws exception
yescls-int1
yescls-int2
yescls-uni
yescls-comthrows exception
yescls-svf1
yescls-svf2
yescls-avf
yescls-hv1
yescls-hv2
nocls-maxc1throws exception
nocls-maxc2
nocls-maxqc1throws exception
nocls-maxqc2throws exception
nocls-maxqc3
nocls-maxqc4
nocls-oo

Class Axiom Semantics

CompletedRule nameNotes
yescax-sco
yescax-eqc1
yescax-eqc2
yescax-dwthrows exception
nocax-adcthrows exception

Schema Vocabulary Semantics

CompletedRule nameNotes
yesscm-eqc1owl:equivalentClassrdfs:subClassOf (one direction)
yesscm-eqc2owl:equivalentClassrdfs:subClassOf (other direction)

Other

  • no datatype semantics for now

Development Notes

To publish new versions of reasonable, tag a commit with the version (e.g. v1.3.2) and push the tag to GitHub. This will execute the publish action which builds an uploads to PyPi.