Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/workflows/python-package-version-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand All @@ -32,7 +32,6 @@ jobs:
# the last release with the old E4/E7/E9/F default. Unpin only together
# with an explicit `select` in pyproject.toml and the matching cleanup.
python -m pip install ruff==0.15.22 pytest
pip install torch==2.6.0
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with ruff
run: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ version: 2
build:
os: ubuntu-22.04
tools:
python: "3.9"
python: "3.10"

sphinx:
configuration: docs/source/conf.py
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<img src="https://raw.githubusercontent.com/lab-v2/pyreason/main/media/pyreason_logo.jpg"/>

[![Python 3.8](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/downloads/release/python-380/)
[![Python 3.9](https://img.shields.io/badge/python-3.9-blue.svg)](https://www.python.org/downloads/release/python-390/)
[![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg)](https://www.python.org/downloads/release/python-3100/)
[![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)](https://www.python.org/downloads/release/python-3110/)
[![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-3120/)
[![Python 3.13](https://img.shields.io/badge/python-3.13-blue.svg)](https://www.python.org/downloads/release/python-3130/)
[![Documentation Status](https://readthedocs.org/projects/pyreason/badge/?version=latest)](https://pyreason.readthedocs.io/en/latest/?badge=latest)
[![pypi](https://github.com/lab-v2/pyreason/actions/workflows/python-publish.yml/badge.svg)](https://github.com/lab-v2/pyreason/actions/workflows/python-publish.yml)
[![Tests](https://github.com/lab-v2/pyreason/actions/workflows/python-package-version-test.yml/badge.svg)](https://github.com/lab-v2/pyreason/actions/workflows/python-package-version-test.yml)
Expand Down Expand Up @@ -43,7 +47,7 @@ PyReason can be installed as a python library using
```bash
pip install pyreason
```
The Python versions that are currently supported are `3.7`, `3.8`, `3.9`, `3.10`. If you want multi-core parallel support only `3.9` and `3.10` versions work due to limited numba support.
The Python versions that are currently supported are `3.8`, `3.9`, `3.10`, `3.11`, `3.12`, and `3.13`.

## 4. Bibtex
If you used this software in your work please cite our paper
Expand Down
4 changes: 2 additions & 2 deletions docs/source/installation.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Installation
==========

PyReason is currently compatible with Python 3.9 and 3.10. To install PyReason, you can use pip:
PyReason is currently compatible with Python 3.8, 3.9, 3.10, 3.11, 3.12, and 3.13. To install PyReason, you can use pip:

.. code:: bash

Expand All @@ -12,7 +12,7 @@ Make sure you're using the correct version of Python. You can create a conda env

.. code:: bash

conda create -n pyreason-env python=3.10
conda create -n pyreason-env python=3.13

PyReason uses a JIT compiler called `Numba <https://numba.pydata.org/>`_ to speed up the reasoning process. This means that
the first time PyReason is imported it will have to compile certain functions which will result in faster runtimes later on.
Expand Down
8 changes: 4 additions & 4 deletions docs/source/tutorials/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Prerequisites

.. note::

Use python version 3.9 or 3.10 .
Use Python version 3.8, 3.9, 3.10, 3.11, 3.12, or 3.13.


Step-by-Step Guide
Expand Down Expand Up @@ -50,19 +50,19 @@ Step-by-Step Guide

.. code-block:: bash

pyenv install 3.8
pyenv install 3.13

4. Create a Virtual Environment

.. code-block:: bash

pyenv virtualenv 3.8 pyreason_venv_3.8
pyenv virtualenv 3.13 pyreason_venv_3.13

5. Activate the Virtual Environment

.. code-block:: bash

pyenv activate pyreason_venv_3.8
pyenv activate pyreason_venv_3.13

6. Install pyreason Using `pip`

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ['setuptools>=42']
requires = ['setuptools>=42', 'setuptools_scm[toml]>=8']
build-backend = 'setuptools.build_meta'

[tool.ruff.lint]
Expand Down
21 changes: 10 additions & 11 deletions pyreason/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# ruff: noqa: F403 F405 (Ignore Pyreason import * for public api)
# Set numba environment variable
import os
import sys
package_path = os.path.abspath(os.path.dirname(__file__))
cache_path = os.path.join(package_path, 'cache')
cache_status_path = os.path.join(package_path, '.cache_status.yaml')
Expand All @@ -9,20 +10,21 @@

from pyreason.pyreason import *
import yaml
from importlib.metadata import version
from pkg_resources import get_distribution, DistributionNotFound
from importlib.metadata import PackageNotFoundError, version

try:
__version__ = get_distribution(__name__).version
except DistributionNotFound:
__version__ = version(__name__)
except PackageNotFoundError:
# package is not installed
pass


with open(cache_status_path) as file:
cache_status = yaml.safe_load(file)

if not cache_status['initialized']:
running_tests = 'pytest' in sys.modules or 'unittest' in sys.modules

if not cache_status['initialized'] and not running_tests:
print('Imported PyReason for the first time. Initializing caches for faster runtimes ... this will take a minute')
graph_path = os.path.join(package_path, 'examples', 'hello-world', 'friends_graph.graphml')

Expand All @@ -37,9 +39,6 @@
print('PyReason initialized!')
print()

# Update cache status (skip under test runners to keep repo file clean)
import sys
if 'pytest' not in sys.modules and 'unittest' not in sys.modules:
cache_status['initialized'] = True
with open(cache_status_path, 'w') as file:
yaml.dump(cache_status, file)
cache_status['initialized'] = True
with open(cache_status_path, 'w') as file:
yaml.dump(cache_status, file)
74 changes: 54 additions & 20 deletions pyreason/scripts/interpretation/interpretation.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,45 @@
edges_to_be_added_type = numba.types.Tuple((numba.types.ListType(node_type), numba.types.ListType(node_type), label.label_type))



@numba.njit(cache=True)
def _filter_pending_node_rules(rules_to_be_applied_node, edges_to_be_added_node_rule, rules_to_be_applied_node_trace, rules_to_remove_idx, atom_trace):
rules_to_be_applied_node_new = numba.typed.List.empty_list(rules_to_be_applied_node_type)
edges_to_be_added_node_rule_new = numba.typed.List.empty_list(edges_to_be_added_type)
rules_to_be_applied_node_trace_new = numba.typed.List.empty_list(rules_to_be_applied_trace_type)
for keep_idx in range(len(rules_to_be_applied_node)):
if keep_idx not in rules_to_remove_idx:
rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx])
if atom_trace:
rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx])
# Node rules do not populate the edge list, so it need not match the rule queue.
# Preserve independent filtering instead of indexing it for each pending rule.
for keep_idx in range(len(edges_to_be_added_node_rule)):
if keep_idx not in rules_to_remove_idx:
edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx])
rules_to_be_applied_node[:] = rules_to_be_applied_node_new.copy()
edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new.copy()
if atom_trace:
rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new.copy()


@numba.njit(cache=True)
def _filter_pending_edge_rules(rules_to_be_applied_edge, edges_to_be_added_edge_rule, rules_to_be_applied_edge_trace, rules_to_remove_idx, atom_trace):
rules_to_be_applied_edge_new = numba.typed.List.empty_list(rules_to_be_applied_edge_type)
edges_to_be_added_edge_rule_new = numba.typed.List.empty_list(edges_to_be_added_type)
rules_to_be_applied_edge_trace_new = numba.typed.List.empty_list(rules_to_be_applied_trace_type)
for keep_idx in range(len(rules_to_be_applied_edge)):
if keep_idx not in rules_to_remove_idx:
rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx])
edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx])
if atom_trace:
rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx])
rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new.copy()
edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new.copy()
if atom_trace:
rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new.copy()


class Interpretation:
specific_node_labels = numba.typed.Dict.empty(key_type=label.label_type, value_type=numba.types.ListType(node_type))
specific_edge_labels = numba.typed.Dict.empty(key_type=label.label_type, value_type=numba.types.ListType(edge_type))
Expand Down Expand Up @@ -438,9 +477,9 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi
# Apply the rules that need to be applied at this timestep
# Nodes
rules_to_remove_idx.clear()
for idx, i in enumerate(rules_to_be_applied_node):
if i[0] == t:
comp, l, bnd, set_static = i[1], i[2], i[3], i[4]
for idx, pending_rule in enumerate(rules_to_be_applied_node):
if pending_rule[0] == t:
comp, l, bnd, set_static = pending_rule[1], pending_rule[2], pending_rule[3], pending_rule[4]
# Check for inconsistencies
if check_consistent_node(interpretations_node, comp, (l, bnd)):
override = True if update_mode == 'override' else False
Expand Down Expand Up @@ -470,16 +509,13 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi
rules_to_remove_idx.add(idx)

# Remove from rules to be applied and edges to be applied lists after coming out from loop
rules_to_be_applied_node[:] = numba.typed.List([rules_to_be_applied_node[i] for i in range(len(rules_to_be_applied_node)) if i not in rules_to_remove_idx])
edges_to_be_added_node_rule[:] = numba.typed.List([edges_to_be_added_node_rule[i] for i in range(len(edges_to_be_added_node_rule)) if i not in rules_to_remove_idx])
if atom_trace:
rules_to_be_applied_node_trace[:] = numba.typed.List([rules_to_be_applied_node_trace[i] for i in range(len(rules_to_be_applied_node_trace)) if i not in rules_to_remove_idx])
_filter_pending_node_rules(rules_to_be_applied_node, edges_to_be_added_node_rule, rules_to_be_applied_node_trace, rules_to_remove_idx, atom_trace)

# Edges
rules_to_remove_idx.clear()
for idx, i in enumerate(rules_to_be_applied_edge):
if i[0] == t:
comp, l, bnd, set_static = i[1], i[2], i[3], i[4]
for idx, pending_rule in enumerate(rules_to_be_applied_edge):
if pending_rule[0] == t:
comp, l, bnd, set_static = pending_rule[1], pending_rule[2], pending_rule[3], pending_rule[4]
sources, targets, edge_l = edges_to_be_added_edge_rule[idx]
edges_added, changes = _add_edges(sources, targets, neighbors, reverse_neighbors, nodes, edges, edge_l, interpretations_node, interpretations_edge, predicate_map_edge, num_ga, t)
changes_cnt += changes
Expand Down Expand Up @@ -544,10 +580,7 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi
rules_to_remove_idx.add(idx)

# Remove from rules to be applied and edges to be applied lists after coming out from loop
rules_to_be_applied_edge[:] = numba.typed.List([rules_to_be_applied_edge[i] for i in range(len(rules_to_be_applied_edge)) if i not in rules_to_remove_idx])
edges_to_be_added_edge_rule[:] = numba.typed.List([edges_to_be_added_edge_rule[i] for i in range(len(edges_to_be_added_edge_rule)) if i not in rules_to_remove_idx])
if atom_trace:
rules_to_be_applied_edge_trace[:] = numba.typed.List([rules_to_be_applied_edge_trace[i] for i in range(len(rules_to_be_applied_edge_trace)) if i not in rules_to_remove_idx])
_filter_pending_edge_rules(rules_to_be_applied_edge, edges_to_be_added_edge_rule, rules_to_be_applied_edge_trace, rules_to_remove_idx, atom_trace)

# Fixed point
if update:
Expand Down Expand Up @@ -1427,15 +1460,15 @@ def get_rule_edge_clause_grounding(clause_var_1, clause_var_2, groundings, groun
# We replace Y by the sources of Z
elif clause_var_1 not in groundings and clause_var_2 in groundings:
for n in groundings[clause_var_2]:
es = numba.typed.List([(nn, n) for nn in reverse_neighbors[n]])
edge_groundings.extend(es)
for nn in reverse_neighbors[n]:
edge_groundings.append((nn, n))

# Case 3:
# We replace Z by the neighbors of Y
elif clause_var_1 in groundings and clause_var_2 not in groundings:
for n in groundings[clause_var_1]:
es = numba.typed.List([(n, nn) for nn in neighbors[n]])
edge_groundings.extend(es)
for nn in neighbors[n]:
edge_groundings.append((n, nn))

# Case 4:
# We have seen both variables before
Expand All @@ -1447,8 +1480,9 @@ def get_rule_edge_clause_grounding(clause_var_1, clause_var_2, groundings, groun
else:
groundings_clause_var_2_set = set(groundings[clause_var_2])
for n in groundings[clause_var_1]:
es = numba.typed.List([(n, nn) for nn in neighbors[n] if nn in groundings_clause_var_2_set])
edge_groundings.extend(es)
for nn in neighbors[n]:
if nn in groundings_clause_var_2_set:
edge_groundings.append((n, nn))

return edge_groundings

Expand Down
Loading
Loading