Skip to content
Closed
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
15 changes: 9 additions & 6 deletions .github/workflows/python-package-version-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10"]
# HEY WE CHANGED THIS — bump from 3.9/3.10 to the PR #163 range.
# 3.14 left out until numpy/torch pins are revalidated there.
python-version: ["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,15 +34,16 @@ 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
# Changed from testing only the source checkout to validating package installation.
python -m pip install -e . --no-deps
- name: Lint with ruff
run: |
python -m ruff check pyreason/scripts
- name: Pytest Unit Tests with JIT Disabled
- name: Pytest Legacy Disable-JIT Unit Suite
run: |
pytest tests/unit/disable_jit --tb=short -q
- name: Pytest Unit Tests with JIT Enabled
- name: Pytest Legacy JIT Unit Suite
run: |
pytest tests/unit/dont_disable_jit --tb=short -q
- name: Pytest API Tests
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<img src="https://raw.githubusercontent.com/lab-v2/pyreason/main/media/pyreason_logo.jpg"/>

[![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 +45,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.10`, `3.11`, `3.12`, `3.13`.

## 4. Bibtex
If you used this software in your work please cite our paper
Expand Down
2 changes: 1 addition & 1 deletion initialize.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Run this script after cloning repository to generate the numba caches. This script runs the hello-world program internally
# Changed: used to warm Numba caches. Now it just runs hello-world once to smoke-check the install.
print('Initializing PyReason caches')
import pyreason as pr

42 changes: 3 additions & 39 deletions pyreason/__init__.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,9 @@
# ruff: noqa: F403 F405 (Ignore Pyreason import * for public api)
# Set numba environment variable
import os
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')
os.environ['NUMBA_CACHE_DIR'] = cache_path


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']:
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')

settings.verbose = False
load_graphml(graph_path)
add_rule(Rule('popular(x) <-1 popular(y), Friends(x,y), owns(y,z), owns(x,z)', 'popular_rule'))
add_fact(Fact('popular(Mary)', 'popular_fact', 0, 2))
reason(timesteps=2)

reset()
reset_rules()
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)
106 changes: 53 additions & 53 deletions pyreason/pyreason.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Changed: Numba is gone on this branch — no imports, no JIT, plain Python lists/dicts.
# This is the file that will be imported when "import pyreason" is called. All content will be run automatically
# ruff: noqa: F401 (Ignore Pyreason import * for public api)
import importlib
import json
import networkx as nx
import numba
import time
import sys
import pandas as pd
Expand Down Expand Up @@ -201,8 +201,9 @@ def store_interpretation_changes(self) -> bool:

@property
def parallel_computing(self) -> bool:
"""Returns whether to use multiple CPU cores for inference. This will disable cacheing and pyreason will have
to be re-compiled at each run - but after compilation it will be faster. Default is False
"""Returns whether to select parallel-compatibility mode. In the pure-Python Fable
engine, this preserves the original parallel mode's reasoning results but
currently uses the regular sequential schedule. Default is False

:return: bool
"""
Expand Down Expand Up @@ -407,10 +408,11 @@ def store_interpretation_changes(self, value: bool) -> None:

@parallel_computing.setter
def parallel_computing(self, value: bool) -> None:
"""Whether to use multiple CPU cores for inference. This will disable cacheing and pyreason will have
to be re-compiled at each run - but after compilation it will be faster. Default is False
"""Whether to select parallel-compatibility mode. In the pure-Python Fable
engine, this preserves the original parallel mode's reasoning results but
currently uses the regular sequential schedule. Default is False

:param value: Whether to make inference run on parallel hardware (multiple CPU cores)
:param value: Whether to select parallel-compatibility mode
:raises TypeError: If not bool raise error
"""
if not isinstance(value, bool):
Expand Down Expand Up @@ -458,21 +460,21 @@ def fp_version(self, value: bool) -> None:

# VARIABLES
__graph: Optional[nx.DiGraph] = None
__rules: Optional[numba.typed.List] = None
__rules: Optional[list] = None
__clause_maps: Optional[dict] = None
__node_facts: Optional[numba.typed.List] = None
__edge_facts: Optional[numba.typed.List] = None
__node_facts: Optional[list] = None
__edge_facts: Optional[list] = None
__facts_name_set = set() # We want to warn the user if they add multiple facts with the same name
__rules_name_set = set() # We want to warn the user if they add multiple rules with the same name
__ipl: Optional[numba.typed.List] = None
__specific_node_labels: Optional[numba.typed.List] = None
__specific_edge_labels: Optional[numba.typed.List] = None
__ipl: Optional[list] = None
__specific_node_labels: Optional[list] = None
__specific_edge_labels: Optional[list] = None
__closed_world_predicates = set()

__non_fluent_graph_facts_node: Optional[numba.typed.List] = None
__non_fluent_graph_facts_edge: Optional[numba.typed.List] = None
__specific_graph_node_labels: Optional[numba.typed.List] = None
__specific_graph_edge_labels: Optional[numba.typed.List] = None
__non_fluent_graph_facts_node: Optional[list] = None
__non_fluent_graph_facts_edge: Optional[list] = None
__specific_graph_node_labels: Optional[list] = None
__specific_graph_edge_labels: Optional[list] = None

__annotation_functions = []
__head_functions = []
Expand Down Expand Up @@ -580,10 +582,10 @@ def load_graphml(path: str) -> None:
if settings.graph_attribute_parsing:
__non_fluent_graph_facts_node, __non_fluent_graph_facts_edge, __specific_graph_node_labels, __specific_graph_edge_labels = __graphml_parser.parse_graph_attributes(settings.static_graph_facts)
else:
__non_fluent_graph_facts_node = numba.typed.List.empty_list(fact_node.fact_type)
__non_fluent_graph_facts_edge = numba.typed.List.empty_list(fact_edge.fact_type)
__specific_graph_node_labels = numba.typed.Dict.empty(key_type=label.label_type, value_type=numba.types.ListType(numba.types.string))
__specific_graph_edge_labels = numba.typed.Dict.empty(key_type=label.label_type, value_type=numba.types.ListType(numba.types.Tuple((numba.types.string, numba.types.string))))
__non_fluent_graph_facts_node = []
__non_fluent_graph_facts_edge = []
__specific_graph_node_labels = {}
__specific_graph_edge_labels = {}


def load_graph(graph: nx.DiGraph) -> None:
Expand All @@ -602,10 +604,10 @@ def load_graph(graph: nx.DiGraph) -> None:
if settings.graph_attribute_parsing:
__non_fluent_graph_facts_node, __non_fluent_graph_facts_edge, __specific_graph_node_labels, __specific_graph_edge_labels = __graphml_parser.parse_graph_attributes(settings.static_graph_facts)
else:
__non_fluent_graph_facts_node = numba.typed.List.empty_list(fact_node.fact_type)
__non_fluent_graph_facts_edge = numba.typed.List.empty_list(fact_edge.fact_type)
__specific_graph_node_labels = numba.typed.Dict.empty(key_type=label.label_type, value_type=numba.types.ListType(numba.types.string))
__specific_graph_edge_labels = numba.typed.Dict.empty(key_type=label.label_type, value_type=numba.types.ListType(numba.types.Tuple((numba.types.string, numba.types.string))))
__non_fluent_graph_facts_node = []
__non_fluent_graph_facts_edge = []
__specific_graph_node_labels = {}
__specific_graph_edge_labels = {}


def load_inconsistent_predicate_list(path: str) -> None:
Expand All @@ -625,7 +627,7 @@ def add_inconsistent_predicate(pred1: str, pred2: str) -> None:
"""
global __ipl
if __ipl is None:
__ipl = numba.typed.List.empty_list(numba.types.Tuple((label.label_type, label.label_type)))
__ipl = []
__ipl.append((label.Label(pred1), label.Label(pred2)))


Expand All @@ -636,7 +638,7 @@ def add_rule(pr_rule: Rule) -> None:

# Add to collection of rules
if __rules is None:
__rules = numba.typed.List.empty_list(rule.rule_type)
__rules = []

# Generate name for rule if not set
if pr_rule.rule.get_rule_name() is None:
Expand Down Expand Up @@ -1139,9 +1141,9 @@ def add_fact(pyreason_fact: Fact) -> None:
global __node_facts, __edge_facts

if __node_facts is None:
__node_facts = numba.typed.List.empty_list(fact_node.fact_type)
__node_facts = []
if __edge_facts is None:
__edge_facts = numba.typed.List.empty_list(fact_edge.fact_type)
__edge_facts = []

if pyreason_fact.type == 'node':
if pyreason_fact.name is None:
Expand Down Expand Up @@ -1415,7 +1417,7 @@ def add_fact_from_csv(csv_path: str, raise_errors = True) -> None:
def add_annotation_function(function: Callable) -> None:
"""Function to add annotation functions to PyReason. The added functions can be used in rules.

The function must be ``@numba.njit``-decorated and must accept exactly one of the two
The function must accept exactly one of the two
supported signatures:

- 2 args (legacy)::
Expand Down Expand Up @@ -1454,7 +1456,7 @@ def fn(annotations, weights,
``TypeError`` here rather than producing a confusing failure inside the reasoning
loop.

:param function: Function to be added. Must be ``@numba.njit``-decorated and must
:param function: Function to be added. Must
match one of the two supported signatures above.
:type function: Callable
:return: None
Expand All @@ -1463,11 +1465,10 @@ def fn(annotations, weights,
"""
# Make sure that the functions are jitted so that they can be passed around in other jitted functions
# TODO: Remove if necessary
# assert hasattr(function, 'nopython_signatures'), 'The function to be added has to be under a `numba.njit` decorator'

# Arity gate: only 2-arg and 6-arg signatures are supported by `annotate`.
# Validating here keeps the error close to the user's call site and avoids
# `raise` inside numba.objmode (which would fail with_lifting).
# Changed: used to worry about raises inside numba.objmode — plain Python now, raise freely.
py_func = getattr(function, 'py_func', function)
nargs = py_func.__code__.co_argcount
if nargs != 2 and nargs != 6:
Expand All @@ -1484,13 +1485,12 @@ def fn(annotations, weights,
def add_head_function(function: Callable) -> None:
"""Function to add head functions to PyReason. The added functions can be used in rules

:param function: Function to be added. This has to be under a numba `njit` decorator. function has signature: one parameter as input -- annotations
:param function: Function to be added. Signature: annotations (and optional weights / grounding args).
:type function: Callable
:return: None
"""
# Make sure that the functions are jitted so that they can be passed around in other jitted functions
# TODO: Remove if necessary
# assert hasattr(function, 'nopython_signatures'), 'The function to be added has to be under a `numba.njit` decorator'
__head_functions.append(function)


Expand Down Expand Up @@ -1551,16 +1551,16 @@ def _reason(timesteps, convergence_threshold, convergence_bound_threshold, queri


if __node_facts is None:
__node_facts = numba.typed.List.empty_list(fact_node.fact_type)
__node_facts = []
if __edge_facts is None:
__edge_facts = numba.typed.List.empty_list(fact_edge.fact_type)
__edge_facts = []

if __ipl is None:
__ipl = numba.typed.List.empty_list(numba.types.Tuple((label.label_type, label.label_type)))
__ipl = []

# Add results of graph parse to existing specific labels and facts
__specific_node_labels = numba.typed.Dict.empty(key_type=label.label_type, value_type=numba.types.ListType(numba.types.string))
__specific_edge_labels = numba.typed.Dict.empty(key_type=label.label_type, value_type=numba.types.ListType(numba.types.Tuple((numba.types.string, numba.types.string))))
__specific_node_labels = {}
__specific_edge_labels = {}
for label_name, nodes in __specific_graph_node_labels.items():
if label_name in __specific_node_labels:
__specific_node_labels[label_name].extend(nodes)
Expand All @@ -1573,18 +1573,18 @@ def _reason(timesteps, convergence_threshold, convergence_bound_threshold, queri
else:
__specific_edge_labels[label_name] = edges

all_node_facts = numba.typed.List.empty_list(fact_node.fact_type)
all_edge_facts = numba.typed.List.empty_list(fact_edge.fact_type)
all_node_facts.extend(numba.typed.List(__node_facts))
all_edge_facts.extend(numba.typed.List(__edge_facts))
all_node_facts = []
all_edge_facts = []
all_node_facts.extend(list(__node_facts))
all_edge_facts.extend(list(__edge_facts))
all_node_facts.extend(__non_fluent_graph_facts_node)
all_edge_facts.extend(__non_fluent_graph_facts_edge)

# Atom trace cannot be true when store interpretations is false
if not settings.store_interpretation_changes:
settings.atom_trace = False

# Convert list of annotation functions into tuple to be numba compatible
# Pack annotation functions into a tuple (stable iterate order)
annotation_functions = tuple(__annotation_functions)
head_functions = tuple(__head_functions)

Expand All @@ -1600,7 +1600,7 @@ def _reason(timesteps, convergence_threshold, convergence_bound_threshold, queri
if settings.verbose:
print('Optimizing rules by moving node clauses ahead of edge clauses')
__rules_copy = __rules.copy()
__rules = numba.typed.List.empty_list(rule.rule_type)
__rules = []
for i, r in enumerate(__rules_copy):
r, __clause_maps[r.get_rule_name()] = reorder_clauses(r)
__rules.append(r)
Expand All @@ -1610,11 +1610,11 @@ def _reason(timesteps, convergence_threshold, convergence_bound_threshold, queri
__program.specific_node_labels = __specific_node_labels
__program.specific_edge_labels = __specific_edge_labels

# Convert closed_world predicates to numba-compatible list of label types
closed_world_preds_numba = numba.typed.List.empty_list(label.label_type)
# Pack closed-world predicate names into Label objects
closed_world_preds = []
for pred_name in __closed_world_predicates:
closed_world_preds_numba.append(label.Label(pred_name))
__program.closed_world_predicates = closed_world_preds_numba
closed_world_preds.append(label.Label(pred_name))
__program.closed_world_predicates = closed_world_preds

# Run Program and get final interpretation
interpretation = __program.reason(timesteps, convergence_threshold, convergence_bound_threshold, settings.verbose)
Expand All @@ -1631,10 +1631,10 @@ def _reason_again(timesteps, restart, convergence_threshold, convergence_bound_t
assert __program is not None, 'To run `reason_again` you need to have reasoned once before'

# Extend facts
all_node_facts = numba.typed.List.empty_list(fact_node.fact_type)
all_edge_facts = numba.typed.List.empty_list(fact_edge.fact_type)
all_node_facts.extend(numba.typed.List(__node_facts))
all_edge_facts.extend(numba.typed.List(__edge_facts))
all_node_facts = []
all_edge_facts = []
all_node_facts.extend(list(__node_facts))
all_edge_facts.extend(list(__edge_facts))

# Run Program and get final interpretation
interpretation = __program.reason_again(timesteps, restart, convergence_threshold, convergence_bound_threshold, all_node_facts, all_edge_facts, settings.verbose)
Expand Down
Loading
Loading