activerdf_rules 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +0 -0
- data/Manifest.txt +13 -0
- data/README.txt +33 -0
- data/Rakefile +49 -0
- data/examples/person_example.rb +67 -0
- data/lib/activerdf_rules/rule_base.rb +195 -0
- data/lib/activerdf_rules/rule_engine.rb +267 -0
- data/lib/activerdf_rules/rule_engine_mixin.rb +34 -0
- data/lib/activerdf_rules/version.rb +9 -0
- data/lib/activerdf_rules.rb +3 -0
- data/setup.rb +1585 -0
- data/test/activerdf_rules/owl_rulebase_test.rb +234 -0
- data/test/activerdf_rules/rdfs_rulebase_test.rb +95 -0
- data/test/activerdf_rules/rule_base_test.rb +103 -0
- data/test/activerdf_rules/rule_engine_mixin_test.rb +19 -0
- data/test/activerdf_rules/rule_engine_test.rb +59 -0
- data/test/activerdf_rules_test.rb +11 -0
- data/test/test_helper.rb +11 -0
- metadata +71 -0
data/History.txt
ADDED
File without changes
|
data/Manifest.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.txt
|
4
|
+
Rakefile
|
5
|
+
setup.rb
|
6
|
+
examples/person_example.rb
|
7
|
+
lib/activerdf_rules.rb
|
8
|
+
lib/activerdf_rules/rule_base.rb
|
9
|
+
lib/activerdf_rules/rule_engine_mixin.rb
|
10
|
+
lib/activerdf_rules/rule_engine.rb
|
11
|
+
lib/activerdf_rules/version.rb
|
12
|
+
test/test_helper.rb
|
13
|
+
test/activerdf_rules_test.rb
|
data/README.txt
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
=What it is?
|
2
|
+
ActiveRDF Rules is a rule base and forward chaining production system for
|
3
|
+
ActiveRDF. It can be used to process rules programatically at will, or using a
|
4
|
+
mix-in class you can have it process rules every time a triple is added to the
|
5
|
+
data store. The rules are defined using a DSL, and stored in memory. It comes
|
6
|
+
with a couple of prebuilt rule bases for RDF Schema and OWL reasoning.
|
7
|
+
|
8
|
+
=What it is not?
|
9
|
+
* It is not high performance.
|
10
|
+
* The rules are stored in memory, so don't add a lot of them.
|
11
|
+
* It supports *some* basic RDF Schema and OWL reasoning through the built-in
|
12
|
+
rule bases, but they are *basic*. Feel free to not use them and just define
|
13
|
+
your own.
|
14
|
+
* It does not support restrictions. I'm probably going to add restriction
|
15
|
+
rules at some point, but I haven't thought through it all just yet. Also, I'm
|
16
|
+
not sure how to handle some cases. Right now, for instance, if a property has
|
17
|
+
a domain specified, then it will conclude that any object that has that
|
18
|
+
property must be of the type specified as the domain, as opposed to restricting
|
19
|
+
only that type to be able to have that property. That's the way I was told to
|
20
|
+
do it!
|
21
|
+
* It is not stable. I mean it's stable; I have tested it pretty thoroghly as
|
22
|
+
far as its current features go, but I reserve the right to change the
|
23
|
+
implementation, interface, and anything else. I might even change the name of
|
24
|
+
the project. If you're that concerned, then fork the code, and keep a copy for
|
25
|
+
yourself. However, I will do my best to make sure that the implementation and
|
26
|
+
interface evolve in a way that it breaks as little existing code as possible.
|
27
|
+
|
28
|
+
=What next?
|
29
|
+
Take a look at the documentation. Especially look at the person_example.rb
|
30
|
+
file. It contains a pretty heavily annotated example. If you have (or will)
|
31
|
+
install this as a gem, then look for that in the gem directory
|
32
|
+
(/usr/lib/ruby/gems/1.8/gems/activerdf-rules-X.X/examples) or some such
|
33
|
+
nonsense.
|
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/clean'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rake/packagetask'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
require 'rake/contrib/rubyforgepublisher'
|
9
|
+
require 'fileutils'
|
10
|
+
require 'hoe'
|
11
|
+
include FileUtils
|
12
|
+
require File.join(File.dirname(__FILE__), 'lib', 'activerdf_rules', 'version')
|
13
|
+
|
14
|
+
AUTHOR = "pjstadig" # can also be an array of Authors
|
15
|
+
EMAIL = "paul@stadig.name"
|
16
|
+
DESCRIPTION = "A rulebase and forward chaining production system for activerdf databases"
|
17
|
+
GEM_NAME = "activerdf_rules" # what ppl will type to install your gem
|
18
|
+
RUBYFORGE_PROJECT = "activerdf-rules" # The unix name for your project
|
19
|
+
HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
|
20
|
+
RELEASE_TYPES = %w( gem ) # can use: gem, tar, zip
|
21
|
+
|
22
|
+
|
23
|
+
NAME = "activerdf_rules"
|
24
|
+
REV = nil # UNCOMMENT IF REQUIRED: File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
|
25
|
+
VERS = ENV['VERSION'] || (ActiveRDFRules::VERSION::STRING + (REV ? ".#{REV}" : ""))
|
26
|
+
CLEAN.include ['**/.*.sw?', '*.gem', '.config']
|
27
|
+
RDOC_OPTS = ['--quiet', '--title', "activerdf_rules documentation",
|
28
|
+
"--opname", "index.html",
|
29
|
+
"--line-numbers",
|
30
|
+
"--main", "README",
|
31
|
+
"--inline-source"]
|
32
|
+
|
33
|
+
# Generate all the Rake tasks
|
34
|
+
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
35
|
+
hoe = Hoe.new(GEM_NAME, VERS) do |p|
|
36
|
+
p.author = AUTHOR
|
37
|
+
p.description = DESCRIPTION
|
38
|
+
p.email = EMAIL
|
39
|
+
p.summary = DESCRIPTION
|
40
|
+
p.url = HOMEPATH
|
41
|
+
p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
|
42
|
+
p.test_globs = ["test/**/*_test.rb"]
|
43
|
+
p.clean_globs = CLEAN #An array of file patterns to delete on clean.
|
44
|
+
|
45
|
+
# == Optional
|
46
|
+
#p.changes - A description of the release's latest changes.
|
47
|
+
#p.extra_deps - An array of rubygem dependencies.
|
48
|
+
#p.spec_extras - A hash of extra values to set in the gemspec.
|
49
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
#=== Initial Setup Code
|
2
|
+
require 'rubygems'
|
3
|
+
require 'active_rdf'
|
4
|
+
require 'activerdf_rules'
|
5
|
+
|
6
|
+
adapter = ConnectionPool.add_data_source :type => :redland
|
7
|
+
Namespace.register(:test, 'http://test.com/')
|
8
|
+
adapter.add(Namespace.lookup(:test, 'Person'), Namespace.lookup(:rdf, 'type'), Namespace.lookup(:rdfs, 'Class'))
|
9
|
+
adapter.add(Namespace.lookup(:test, 'eye'), Namespace.lookup(:rdf, 'type'), Namespace.lookup(:rdf, 'Property'))
|
10
|
+
adapter.add(Namespace.lookup(:test, 'eye'), Namespace.lookup(:rdfs, 'domain'), Namespace.lookup(:test, 'Person'))
|
11
|
+
adapter.add(Namespace.lookup(:test, 'eye'), Namespace.lookup(:rdfs, 'range'), Namespace.lookup(:test, 'Color'))
|
12
|
+
ObjectManager.construct_classes
|
13
|
+
|
14
|
+
#=== Here is the real example
|
15
|
+
# First we add couple of resources to the database.
|
16
|
+
paul = TEST::Person.new Namespace.expand(:test, 'Paul')
|
17
|
+
paul.save
|
18
|
+
|
19
|
+
blue = Namespace.lookup(:test, 'blue')
|
20
|
+
blue.save
|
21
|
+
|
22
|
+
# Now we set paul's eye property to blue.
|
23
|
+
paul.eye = blue
|
24
|
+
|
25
|
+
# The next line should simple read '[RDFS::Resource]'.
|
26
|
+
p blue.type
|
27
|
+
|
28
|
+
# Now we'll see how you can use the rule engine to manually process rules.
|
29
|
+
re = RuleEngine.new :rule_base => RuleEngine::RDFSRuleBase
|
30
|
+
|
31
|
+
# Calling process_rules once will try every rule once.
|
32
|
+
re.process_rules
|
33
|
+
|
34
|
+
# Calling process_rules in a loop will process all of the rules until nothing
|
35
|
+
# more can be concluded. This works because process_rules returns true if it
|
36
|
+
# added new information.
|
37
|
+
while re.process_rules; end
|
38
|
+
|
39
|
+
# Now we can see that blue's type will include TEST::Color because one of the
|
40
|
+
# RDFS rules fired.
|
41
|
+
p blue.type
|
42
|
+
|
43
|
+
#=== Example two
|
44
|
+
# Say you wanted to have the rule engine automatically process all of the rules
|
45
|
+
# every time a new triple is added to the database. There are two ways of
|
46
|
+
# doing this: first, you can re-open the class and include the mix-in
|
47
|
+
#
|
48
|
+
# class RedlandAdapter
|
49
|
+
# include RuleEngineMixin
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# Second, you can just add the mix-in to an objects meta-class like below, if
|
53
|
+
# for example you don't want every instance of the adapter to have rule
|
54
|
+
# processing.
|
55
|
+
class << adapter
|
56
|
+
include RuleEngineMixin
|
57
|
+
end
|
58
|
+
adapter.rule_engine = RuleEngine.new :rule_base => RuleEngine::RDFSRuleBase
|
59
|
+
red = Namespace.lookup(:test, 'red')
|
60
|
+
red.save
|
61
|
+
|
62
|
+
p red.type
|
63
|
+
paul.eye = red
|
64
|
+
|
65
|
+
# Now by just adding the triple to the database, we automatically have our
|
66
|
+
# rules fire.
|
67
|
+
p red.type
|
@@ -0,0 +1,195 @@
|
|
1
|
+
# An instance of the RuleBase class contains a set of rules for processing with
|
2
|
+
# the RuleEngine. It has a built in DSL that can be used to create a rule
|
3
|
+
# base.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
# rb = RuleBase.new do
|
7
|
+
# rule 'Rule1' do
|
8
|
+
# condition :x, :y, :z
|
9
|
+
#
|
10
|
+
# conclusion :z, :y, :x
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# rule 'Rule2' do
|
14
|
+
# condition :x, :y, :z
|
15
|
+
#
|
16
|
+
# conclusion :y, :x, :z
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# Additionally, you can create an instance of RuleBase and just add rules to
|
21
|
+
# the +rules+ array manually
|
22
|
+
#
|
23
|
+
# Example:
|
24
|
+
# rb = RuleBase.new 'DSLRuleBase'
|
25
|
+
# r = RuleBase::Rule.new 'Rule 1'
|
26
|
+
# r.condition :x, :y, :z
|
27
|
+
# r.conclusion :z, :y, :x
|
28
|
+
# rb.rules << r
|
29
|
+
#
|
30
|
+
# As you can see from these examples you can create variables in your rules by
|
31
|
+
# using symbols.
|
32
|
+
#
|
33
|
+
# There is no point in adding more than one rule that has the same conditions
|
34
|
+
# and conclusion (i.e. they do the same thing), because it will just mean that
|
35
|
+
# the RuleEngine will process an extra rule with no extra benefit.
|
36
|
+
#
|
37
|
+
# Currently, RuleBase does not verify that you are adding unique rules.
|
38
|
+
# However, it does verify that the conclusion and all of the conditions are
|
39
|
+
# triples. It also verifies that the subject and predicate of the conclusion
|
40
|
+
# and every condition are either variables or resources. Additionally, it only
|
41
|
+
# allows you to set one conclusion. If any of these conditions is not met,
|
42
|
+
# then it will throw an exception.
|
43
|
+
#
|
44
|
+
# When using the class as a DSL it will also check each rule as it is added to
|
45
|
+
# ensure that all of the variables that appear in the conclusion also appear in
|
46
|
+
# at least one condition. If this condition is not met it will throw an
|
47
|
+
# exception. When adding rules manually to the +rules+ array it will not
|
48
|
+
# perform this check. However, later on when the RuleEngine converts the rule
|
49
|
+
# to a query ActiveRDF will throw an exception if this condition is not met.
|
50
|
+
class RuleBase
|
51
|
+
attr_accessor :name, :rules
|
52
|
+
|
53
|
+
# Any block that is passed in will be instance_eval'ed, which will allow you
|
54
|
+
# to use the built in DSL.
|
55
|
+
def initialize(name = nil, &b)
|
56
|
+
@name = name || 'NoName'
|
57
|
+
@rules = []
|
58
|
+
instance_eval(&b) if b
|
59
|
+
end
|
60
|
+
|
61
|
+
# Used in the DSL method to add a rule to this rule base.
|
62
|
+
def rule(name = nil, &b)
|
63
|
+
r = Rule.new name || "Rule #{@rules.size + 1}"
|
64
|
+
r.instance_eval(&b)
|
65
|
+
r.validate
|
66
|
+
@rules << r
|
67
|
+
$activerdflog.debug("Adding '#{r}' to '#{self}'")
|
68
|
+
end
|
69
|
+
|
70
|
+
# Makes a deep copy of the rule base, including the +rules+ array.
|
71
|
+
def dup
|
72
|
+
rb = super
|
73
|
+
rb.rules = self.rules.dup
|
74
|
+
rb
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns the name of the rule base
|
78
|
+
def to_s
|
79
|
+
@name
|
80
|
+
end
|
81
|
+
|
82
|
+
# A rule for adding information to an ActiveRDF store. A rule consists of a
|
83
|
+
# set of conditions and a single conclusion. Each condition consists of a
|
84
|
+
# triple (with variables) that is matched against the database. When all of
|
85
|
+
# the conditions are satisfied, then the conclusion is introduced into the
|
86
|
+
# database substituting for any variables that may appear in the conclusion.
|
87
|
+
#
|
88
|
+
# Variables are simply a ruby symbols and they can be used within both the
|
89
|
+
# conditions and the conclusion with the following caveat: any variable that
|
90
|
+
# appears in the conclusion must also appear in at least one condition.
|
91
|
+
#
|
92
|
+
# Example:
|
93
|
+
# r = RuleBase::Rule.new
|
94
|
+
# r.condition :x, :y, :z
|
95
|
+
# r.conclusion :z, :y, :x
|
96
|
+
class Rule
|
97
|
+
attr_reader :name, :conditions
|
98
|
+
|
99
|
+
def initialize(name)
|
100
|
+
@name = name
|
101
|
+
@conditions = []
|
102
|
+
@conclusion = nil
|
103
|
+
end
|
104
|
+
|
105
|
+
# Adds a condition to the rule where s, p, and o are the subject,
|
106
|
+
# predicate, and object (respectively).
|
107
|
+
def condition(s, p, o)
|
108
|
+
raise "Condition subject #{s} must be a Symbol or an RDFS::Resource" unless [Symbol, RDFS::Resource].include?(s.class)
|
109
|
+
raise "Condition subject #{p} must be a Symbol or an RDFS::Resource" unless [Symbol, RDFS::Resource].include?(p.class)
|
110
|
+
c = [s, p, o]
|
111
|
+
@conditions << c
|
112
|
+
$activerdflog.debug("Adding condition #{c.inspect} to '#{self}'")
|
113
|
+
end
|
114
|
+
|
115
|
+
# Sets the conclusion of the rule where s, p, and o are the subject,
|
116
|
+
# predicate, and object (respectively).
|
117
|
+
def conclusion(s, p, o)
|
118
|
+
raise "Conclusion already set" unless @conclusion.nil?
|
119
|
+
raise "Conclusion subject #{s} must be a Symbol or an RDFS::Resource" unless [Symbol, RDFS::Resource].include?(s.class)
|
120
|
+
raise "Conclusion subject #{p} must be a Symbol or an RDFS::Resource" unless [Symbol, RDFS::Resource].include?(p.class)
|
121
|
+
c = [s, p, o]
|
122
|
+
@conclusion = c
|
123
|
+
$activerdflog.debug("Setting conclusion #{c.inspect} for '#{self}'")
|
124
|
+
end
|
125
|
+
|
126
|
+
# Checks that every variable in the conclusion also appears in at least on
|
127
|
+
# condition. If this condition is not satisfied, then an exception is
|
128
|
+
# thrown.
|
129
|
+
def validate
|
130
|
+
# for each conclusion variable verify that it appears in one of the conditions
|
131
|
+
@conclusion.each do |x|
|
132
|
+
if x.is_a?(Symbol)
|
133
|
+
catch(:done) do
|
134
|
+
@conditions.each do |c|
|
135
|
+
throw :done if c.include?(x)
|
136
|
+
end
|
137
|
+
raise "Unbound variable :#{x} in conclusion"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Processes this rule by converting it to a query, if the query is
|
144
|
+
# successful, then the conclusion is introduced into the data store after
|
145
|
+
# substituting for any variables.
|
146
|
+
def process
|
147
|
+
$activerdflog.debug("processing '#{self}'")
|
148
|
+
q = Query.new
|
149
|
+
lookup = @conclusion.select{|c| c.is_a?(Symbol)}.uniq
|
150
|
+
q.select(*lookup)
|
151
|
+
@conditions.each do |c|
|
152
|
+
q.where(*c)
|
153
|
+
end
|
154
|
+
|
155
|
+
change = false
|
156
|
+
q.execute.each do |row|
|
157
|
+
triple = @conclusion.collect {|t|
|
158
|
+
instantiate(t, row, lookup)
|
159
|
+
}
|
160
|
+
old = ConnectionPool.write_adapter.size
|
161
|
+
$activerdflog.debug("concluding #{triple.inspect}")
|
162
|
+
ConnectionPool.write_adapter.add(*triple)
|
163
|
+
# TODO HACK This should be changed
|
164
|
+
# What I should be able to do is something like below, but the below is
|
165
|
+
# broken for redland, and the above condition query is broken in
|
166
|
+
# rdflite (because of ambiguous column names). For now this works.
|
167
|
+
# Query.new.ask.where(*triple).execute
|
168
|
+
if old < ConnectionPool.write_adapter.size
|
169
|
+
change = true
|
170
|
+
end
|
171
|
+
end
|
172
|
+
change
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns either the value for +var+ by using +lookup+, the value of +row+,
|
176
|
+
# if the query resulted in a single value, or the value of +var+ since in
|
177
|
+
# this case it must be either an +RDFS::Resource+ or a literal.
|
178
|
+
def instantiate(var, row, lookup)
|
179
|
+
if var.is_a?(Symbol)
|
180
|
+
if lookup.size == 1 && !row.is_a?(Array)
|
181
|
+
row
|
182
|
+
else
|
183
|
+
row[lookup.index(var)]
|
184
|
+
end
|
185
|
+
else
|
186
|
+
var
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Returns the name of this rule
|
191
|
+
def to_s
|
192
|
+
@name
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,267 @@
|
|
1
|
+
# A RuleEngine will use a RuleBase to add new data to an ActiveRDF data store
|
2
|
+
# by processing the rules.
|
3
|
+
# TODO
|
4
|
+
class RuleEngine
|
5
|
+
attr_accessor :rule_base
|
6
|
+
|
7
|
+
# A RuleBase can be specified using the hash key :rule_base.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
# RuleEngine.new :rule_base => RuleEngine::RDFSRuleBase
|
11
|
+
def initialize(values = {})
|
12
|
+
@rule_base = values[:rule_base]
|
13
|
+
end
|
14
|
+
|
15
|
+
def define_rules(&b)
|
16
|
+
rb = RuleBase.new(&b)
|
17
|
+
self.rule_base = rb
|
18
|
+
end
|
19
|
+
|
20
|
+
def rule_base=(rb)
|
21
|
+
if @rule_base.nil?
|
22
|
+
@rule_base = rb.dup
|
23
|
+
else
|
24
|
+
if rb.nil?
|
25
|
+
@rule_base = nil
|
26
|
+
else
|
27
|
+
@rule_base.rules.push(*rb.rules)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def process_rules
|
33
|
+
$activerdflog.debug("processing '#{@rule_base}'")
|
34
|
+
change = false
|
35
|
+
@rule_base.rules.each do |r|
|
36
|
+
change |= r.process
|
37
|
+
end
|
38
|
+
change
|
39
|
+
end
|
40
|
+
|
41
|
+
RDFSRuleBase = RuleBase.new 'RDFSRuleBase' do
|
42
|
+
type = Namespace.lookup(:rdf, 'type')
|
43
|
+
rdfclass = Namespace.lookup(:rdfs, 'Class')
|
44
|
+
subClassOf = Namespace.lookup(:rdfs, 'subClassOf')
|
45
|
+
property = Namespace.lookup(:rdf, 'Property')
|
46
|
+
subPropertyOf = Namespace.lookup(:rdfs, 'subPropertyOf')
|
47
|
+
range = Namespace.lookup(:rdfs, 'range')
|
48
|
+
domain = Namespace.lookup(:rdfs, 'domain')
|
49
|
+
|
50
|
+
# The following reasoning rules where taken from
|
51
|
+
# http://www-ksl.stanford.edu/software/jtp/doc/owl-reasoning.html
|
52
|
+
|
53
|
+
# Type inheritance through rdfs:subclassOf
|
54
|
+
rule do
|
55
|
+
condition :Morris, type, :Cat
|
56
|
+
condition :Cat, subClassOf, :Mammal
|
57
|
+
|
58
|
+
conclusion :Morris, type, :Mammal
|
59
|
+
end
|
60
|
+
|
61
|
+
# Reflexivity of rdfs:subPropertyOf and rdfs:subclassOf
|
62
|
+
rule do
|
63
|
+
condition :p, type, property
|
64
|
+
|
65
|
+
conclusion :p, subPropertyOf, :p
|
66
|
+
end
|
67
|
+
|
68
|
+
rule do
|
69
|
+
condition :p, type, rdfclass
|
70
|
+
|
71
|
+
conclusion :p, subClassOf, :p
|
72
|
+
end
|
73
|
+
|
74
|
+
# Type inference through rdfs:range and rdfs:domain constraints
|
75
|
+
rule do
|
76
|
+
condition :teaches, domain, :Teacher
|
77
|
+
condition :Bob, :teaches, :Scooter
|
78
|
+
|
79
|
+
conclusion :Bob, type, :Teacher
|
80
|
+
end
|
81
|
+
|
82
|
+
rule do
|
83
|
+
condition :teaches, range, :Student
|
84
|
+
condition :Bob, :teaches, :Scooter
|
85
|
+
|
86
|
+
conclusion :Scooter, type, :Student
|
87
|
+
end
|
88
|
+
|
89
|
+
# Transitivity of rdfs:subClassOf and rdfs:subPropertyOf
|
90
|
+
rule do
|
91
|
+
condition :Dog, subClassOf, :Mammal
|
92
|
+
condition :Mammal, subClassOf, :Animal
|
93
|
+
|
94
|
+
conclusion :Dog, subClassOf, :Animal
|
95
|
+
end
|
96
|
+
|
97
|
+
rule do
|
98
|
+
condition :parent, subPropertyOf, :ancestor
|
99
|
+
condition :ancestor, subPropertyOf, :relative
|
100
|
+
|
101
|
+
conclusion :parent, subPropertyOf, :relative
|
102
|
+
end
|
103
|
+
end.freeze
|
104
|
+
RDFSRuleBase.rules.freeze
|
105
|
+
|
106
|
+
OWLRuleBase = RuleBase.new 'OWLRuleBase' do
|
107
|
+
type = Namespace.lookup(:rdf, 'type')
|
108
|
+
transProp = Namespace.lookup(:owl, 'TransitiveProperty')
|
109
|
+
symProp = Namespace.lookup(:owl, 'SymmetricProperty')
|
110
|
+
inverseOf = Namespace.lookup(:owl, 'inverseOf')
|
111
|
+
disjointWith = Namespace.lookup(:owl, 'disjointWith')
|
112
|
+
subClassOf = Namespace.lookup(:rdfs, 'subClassOf')
|
113
|
+
sameAs = Namespace.lookup(:owl, 'sameAs')
|
114
|
+
rdfclass = Namespace.lookup(:rdfs, 'Class')
|
115
|
+
equivClass = Namespace.lookup(:owl, 'equivalentClass')
|
116
|
+
complementOf = Namespace.lookup(:owl, 'complementOf')
|
117
|
+
funcProp = Namespace.lookup(:owl, 'FunctionalProperty')
|
118
|
+
invFuncProp = Namespace.lookup(:owl, 'InverseFunctionalProperty')
|
119
|
+
onProperty = Namespace.lookup(:owl, 'onProperty')
|
120
|
+
hasValue = Namespace.lookup(:owl, 'hasValue')
|
121
|
+
allValuesFrom = Namespace.lookup(:owl, 'allValuesFrom')
|
122
|
+
someValuesFrom = Namespace.lookup(:owl, 'someValuesFrom')
|
123
|
+
|
124
|
+
# The following reasoning rules where taken from
|
125
|
+
# http://www-ksl.stanford.edu/software/jtp/doc/owl-reasoning.html
|
126
|
+
|
127
|
+
# Enforcing transitivity of owl:TransitiveProperty.
|
128
|
+
rule do
|
129
|
+
condition :ancestor, type, transProp
|
130
|
+
condition :Sue, :ancestor, :Mary
|
131
|
+
condition :Mary, :ancestor, :Anne
|
132
|
+
|
133
|
+
conclusion :Sue, :ancestor, :Anne
|
134
|
+
end
|
135
|
+
|
136
|
+
# Semantics of owl:SymmetricProperty is enforced.
|
137
|
+
rule do
|
138
|
+
condition :relative, type, symProp
|
139
|
+
condition :Sue, :relative, :Mary
|
140
|
+
|
141
|
+
conclusion :Mary, :relative, :Sue
|
142
|
+
end
|
143
|
+
|
144
|
+
# Reasoning with owl:inverseOf.
|
145
|
+
rule do
|
146
|
+
condition :parentOf, inverseOf, :hasParent
|
147
|
+
condition :Goldie, :parentOf, :Kate
|
148
|
+
|
149
|
+
conclusion :Kate, :hasParent, :Goldie
|
150
|
+
end
|
151
|
+
|
152
|
+
# Inheritance of disjointness constraints.
|
153
|
+
rule do
|
154
|
+
condition :Plant, disjointWith, :Animal
|
155
|
+
condition :Mammal, subClassOf, :Animal
|
156
|
+
|
157
|
+
conclusion :Plant, disjointWith, :Mammal
|
158
|
+
end
|
159
|
+
|
160
|
+
# When an owl:sameAs relationship is asserted or inferred between two
|
161
|
+
# entities that are known to be classes, an owl:equivalentClass
|
162
|
+
# relationship is inferred between the classes. Similarly, when an
|
163
|
+
# owl:sameAs relationship is asserted or inferred between two entities that
|
164
|
+
# are known to be properties, an owl:equivalentProperty relationship is
|
165
|
+
# inferred between the classes.
|
166
|
+
rule do
|
167
|
+
condition :Human, sameAs, :Person
|
168
|
+
condition :Human, type, rdfclass
|
169
|
+
condition :Person, type, rdfclass
|
170
|
+
|
171
|
+
conclusion :Human, equivClass, :Person
|
172
|
+
end
|
173
|
+
|
174
|
+
# All the subclasses of a given class are disjoint with the class's
|
175
|
+
# complement.
|
176
|
+
rule do
|
177
|
+
condition :Animal, complementOf, :NonAnimals
|
178
|
+
condition :Mammal, subClassOf, :Animal
|
179
|
+
|
180
|
+
conclusion :Mammal, disjointWith, :NonAnimals
|
181
|
+
end
|
182
|
+
|
183
|
+
# A complicated bit of reasoning about owl:complementOf captured by
|
184
|
+
# following KIF axiom.
|
185
|
+
rule do
|
186
|
+
condition :c1, complementOf, :c2
|
187
|
+
condition :c3, subClassOf, :c1
|
188
|
+
condition :c4, subClassOf, :c2
|
189
|
+
condition :c4, complementOf, :c5
|
190
|
+
|
191
|
+
conclusion :c3, subClassOf, :c5
|
192
|
+
end
|
193
|
+
|
194
|
+
# Inferring owl:sameAs relationships via owl:FunctionalProperty and
|
195
|
+
# owl:InverseFunctionalProperty.
|
196
|
+
rule do
|
197
|
+
condition :mother, type, funcProp
|
198
|
+
condition :Joe, :mother, :Margaret
|
199
|
+
condition :Joe, :mother, :Maggie
|
200
|
+
|
201
|
+
conclusion :Margaret, sameAs, :Maggie
|
202
|
+
end
|
203
|
+
|
204
|
+
rule do
|
205
|
+
condition :motherOf, type, invFuncProp
|
206
|
+
condition :Margaret, :motherOf, :Joe
|
207
|
+
condition :Maggie, :motherOf, :Joe
|
208
|
+
|
209
|
+
conclusion :Margaret, sameAs, :Maggie
|
210
|
+
end
|
211
|
+
|
212
|
+
# TODO If a class A is owl:oneOf a list of objects, say X, Y, and Z, then
|
213
|
+
# each of X, Y, and Z has rdf:type A.
|
214
|
+
|
215
|
+
# If an object is rdf:type an owl:hasValue owl:Restriction, then the object
|
216
|
+
# has the specified value for the specified property.
|
217
|
+
rule do
|
218
|
+
condition :RestrictionOrangeSkin, onProperty, :skinColor
|
219
|
+
condition :RestrictionOrangeSkin, hasValue, :Orange
|
220
|
+
condition :MrOompaLoompa, type, :RestrictionOrangeSkin
|
221
|
+
|
222
|
+
conclusion :MrOompaLoompa, :skinColor, :Orange
|
223
|
+
end
|
224
|
+
|
225
|
+
# If an owl:hasValue owl:Restriction restricts a particular property to a
|
226
|
+
# particular value, and an object has that value for that property, then
|
227
|
+
# the object has the Restriction as a type.
|
228
|
+
rule do
|
229
|
+
condition :RestrictionOrangeSkin, onProperty, :skinColor
|
230
|
+
condition :RestrictionOrangeSkin, hasValue, :Orange
|
231
|
+
condition :MrOompaLoompa, :skinColor, :Orange
|
232
|
+
|
233
|
+
conclusion :MrOompaLoompa, type, :RestrictionOrangeSkin
|
234
|
+
end
|
235
|
+
|
236
|
+
# If an object is a rdf:type an owl:allValuesFrom owl:Restriction, and the
|
237
|
+
# object has values for the specified property, then the values are of the
|
238
|
+
# specified type.
|
239
|
+
rule do
|
240
|
+
condition :RestrictionCatChildren, onProperty, :child
|
241
|
+
condition :RestrictionCatChildren, allValuesFrom, :Cat
|
242
|
+
condition :Fluffy, type, :RestrictionCatChildren
|
243
|
+
condition :Fluffy, :child, :Cupcake
|
244
|
+
|
245
|
+
conclusion :Cupcake, type, :Cat
|
246
|
+
end
|
247
|
+
|
248
|
+
# If an owl:someValuesFrom owl:Restriction restricts a particular property
|
249
|
+
# to a particular type, and if an object has some values of the specificied
|
250
|
+
# type for the specified property, then that object has the Restriction as
|
251
|
+
# a type.
|
252
|
+
rule do
|
253
|
+
condition :RestrictionIvyLeagueDegree, onProperty, :degree
|
254
|
+
condition :RestrictionIvyLeagueDegree, someValuesFrom, :IvyLeagueSchool
|
255
|
+
condition :Mary, :degree, :Harvard
|
256
|
+
condition :Harvard, type, :IvyLeagueSchool
|
257
|
+
|
258
|
+
conclusion :Mary, type, :RestrictionIvyLeagueDegree
|
259
|
+
end
|
260
|
+
|
261
|
+
# TODO If a property Q is owl:inverseOf of a property P, and P is an
|
262
|
+
# owl:TransitiveProperty, then Q is also an owl:TransitiveProperty.
|
263
|
+
|
264
|
+
# TODO All of the elements of an owl:AllDifferent are owl:differentFrom each other.
|
265
|
+
end.freeze
|
266
|
+
OWLRuleBase.rules.freeze
|
267
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# This module may be included in any writeable ActiveRDF adapter in order to
|
2
|
+
# have automatic rule processing every time a triple is added to the database.
|
3
|
+
#
|
4
|
+
# Example:
|
5
|
+
# class << adapter
|
6
|
+
# include RuleEngineMixin
|
7
|
+
# end
|
8
|
+
# adapter.rule_engine = RuleEngine.new :rule_base => RuleEngine::RDFSRuleBase
|
9
|
+
#
|
10
|
+
# Notice that you must set the +rule_engine+ attribute after the module is
|
11
|
+
# mixed in. If you do not specify a RuleEngine, then automatic rule processing
|
12
|
+
# will NOT take place.
|
13
|
+
#
|
14
|
+
# There are two included RuleBases one for RDF Schema, and one for OWL.
|
15
|
+
module RuleEngineMixin
|
16
|
+
def self.included(base)
|
17
|
+
base.class_eval do
|
18
|
+
attr_accessor :rule_engine
|
19
|
+
attr_accessor :disable_rules
|
20
|
+
alias _add add
|
21
|
+
|
22
|
+
#TODO
|
23
|
+
def add(*a)
|
24
|
+
r = _add(*a)
|
25
|
+
unless @disable_rules || @rule_engine.nil?
|
26
|
+
@disable_rules = true
|
27
|
+
while @rule_engine.process_rules; end
|
28
|
+
@disable_rules = false
|
29
|
+
end
|
30
|
+
r
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|