PyReason Annotation Functions
In this tutorial, we will look at use annotation functions in PyReason. Read more about annotation functions here.
Note
Find the full, executable code for both annotation functions here
Average Annotation Function Example
This example takes the average of the lower and higher bounds of the nodes in the graph.
Graph
This example will use a graph created with 2 facts, and only 2 nodes. The annotation functions can be run on a graph of any size. See PyReason Graphs for more information on how to create graphs in PyReason.
Facts
To initialize this graph, we will add 2 nodes P(A) and P(B), using add_fact:
import pyreason as pr
pr.add_fact(pr.Fact('P(A) : [0.01, 1]'))
pr.add_fact(pr.Fact('P(B) : [0.2, 1]'))
Average Annotation Function
Next, we will then add the annotation function to find the average of all the upper and lower bounds of the graph.
Here is the Average Annotation Function:
@numba.njit
def avg_ann_fn(annotations, weights):
# annotations contains the bounds of the atoms that were used to ground the rule. It is a nested list that contains a list for each clause
# You can access for example the first grounded atom's bound by doing: annotations[0][0].lower or annotations[0][0].upper
# We want the normalised sum of the bounds of the grounded atoms
sum_upper_bounds = 0
sum_lower_bounds = 0
num_atoms = 0
for clause in annotations:
for atom in clause:
sum_lower_bounds += atom.lower
sum_upper_bounds += atom.upper
num_atoms += 1
a = sum_lower_bounds / num_atoms
b = sum_upper_bounds / num_atoms
return a, b
This takes the annotations, or a list of the bounds of the grounded atoms and the weights of the grounded atoms, and returns the average of the upper and lower bounds repectivley.
Next, we add this function in PyReason:
pr.add_annotation_function(avg_ann_fn)
Rules
After we have created the graph, and added the annotation function, we add the annotation function to a Rule.
Create Rules of this general format when using an annotation function:
'average_function(A, B):avg_ann_fn <- P(A):[0, 1], P(B):[0, 1]'
The annotation function will be called when all clauses in the rule have been satisfied and the head of the rule is to be annotated.
pr.add_rule(pr.Rule('average_function(A, B):avg_ann_fn <- P(A):[0, 1], P(B):[0, 1]', infer_edges=True))
Running PyReason
Begin the PyReason reasoning process with the added annotation function with:
interpretation = pr.reason(timesteps=1)
Expected Output
The expected output of this function is
Timestep: 0
Converged at time: 0
Fixed Point iterations: 2
TIMESTEP - 0
component average_function
0 (A, B) [0.10500000000000001, 1.0]
- In this output:
The lower bound of the
avg_ann_fn(A, B)is computed as0.105, based on the weighted combination of the lower bounds ofP(A)(0.01) andP(B)(0.2), averaged together.The upper bound of the
linear_combination_function(A, B)is computed as0.4, based on the weighted combination of the upper bounds ofP(A)(1.0) andP(B)(1.0), averaged together.
Linear Combination Annotation Function
Now, we will define and use a new annotation function to compute a weighted linear combination of the bounds of grounded atoms in a rule.
The map_interval Function
We will first define a helper function that maps a value from the interval [lower, upper] to the interval [0, 1]. This will be used in the main annotation function to normalize the bounds:
@numba.njit
def map_interval(t, a, b, c, d):
"""
Maps a value `t` from the interval [a, b] to the interval [c, d] using the formula:
f(t) = c + ((d - c) / (b - a)) * (t - a)
Parameters:
- t: The value to be mapped.
- a: The lower bound of the original interval.
- b: The upper bound of the original interval.
- c: The lower bound of the target interval.
- d: The upper bound of the target interval.
Returns:
- The value `t` mapped to the new interval [c, d].
"""
# Apply the formula to map the value t
mapped_value = c + ((d - c) / (b - a)) * (t - a)
return mapped_value
Graph
This example will use a graph created with 2 facts, and only 2 nodes. The annotation functions can be run on a graph of any size. See PyReason Graphs for more information on how to create graphs in PyReason.
Facts
To initialize this graph, we will add 3 nodes A, B, and C, using add_fact:
import pyreason as pr
pr.add_fact(pr.Fact('A : [.1, 1]'))
pr.add_fact(pr.Fact('B : [.2, 1]'))
pr.add_fact(pr.Fact('C : [.4, 1]'))
Linear Combination Function
Next, we define the annotation function that computes a weighted linear combination of the mapped lower and upper bounds of the grounded atoms. The weights are applied to normalize the values. For simplicity sake, we define the constant at 0.2 within the function, this is alterable for any constant, or for the weights in the graph.
@numba.njit
def lin_comb_ann_fn(annotations, weights):
sum_lower_comb = 0
sum_upper_comb = 0
num_atoms = 0
constant = 0.2
# Iterate over the clauses in the rule
for clause in annotations:
for atom in clause:
# Apply the constant weight to the lower and upper bounds, and accumulate
sum_lower_comb += constant * atom.lower
sum_upper_comb += constant * atom.upper
num_atoms += 1
#if the lower and upper are equal, return [0,1]
if sum_lower_comb == sum_upper_comb:
return 0,1
if sum_lower_comb> sum_upper_comb:
sum_lower_comb,sum_upper_comb= sum_upper_comb, sum_lower_comb
if sum_upper_comb>1:
sum_lower_comb = map_interval(sum_lower_comb, sum_lower_comb, sum_upper_comb, 0,1)
sum_upper_comb = map_interval(sum_upper_comb, sum_lower_comb, sum_lower_comb,0,1)
# Return the weighted linear combination of the lower and upper bounds
return sum_lower_comb, sum_upper_comb
We now add the new annotation function within the PyReason framework:
# Register the custom annotation function with PyReason
pr.add_annotation_function(lin_comb_ann_fn)
Rules
After we have created the graph, and added the annotation function, we add the annotation function to a Rule.
Create Rules of this general format when using an annotation function:
linear_combination_function(A, B):lin_comb_ann_fn <- A:[0, 1], B:[0, 1], C:[0, 1]
pr.add_rule(pr.Rule('linear_combination_function(A, B):lin_comb_ann_fn <- A:[0, 1], B:[0, 1], C:[0, 1]', infer_edges=True))
The annotation function will be called when all clauses in the rule have been satisfied and the head of the Rule is to be annotated.
Running PyReason
Begin the PyReason reasoning process with the added annotation function with:
interpretation = pr.reason(timesteps=1)
Expected Output
Below is the expected output from running the linear_combination_annotation_function:
Timestep: 0
Converged at time: 0
Fixed Point iterations: 2
TIMESTEP - 0
component linear_combination_function
0 (A, B) [0.24000000000000005, 0.6000000000000001]
- In this output:
The lower bound of the
linear_combination_function(A, B, C)is computed as0.24000000000000005, based on the weighted combination of the lower bounds ofA(0.1),B(0.2), andC(0.4) multiplied by the constant(0.2) then added together.The upper bound of the
linear_combination_function(A, B, C)is computed as0.6000000000000001, based on the weighted combination of the upper bounds ofA(1),B(1), andC(1) multiplied by the constant(0.2) then added together.