From 4535531bdb969423e81d3c5cd96697ae6d1ab6eb Mon Sep 17 00:00:00 2001 From: BAF1927 Date: Tue, 8 Sep 2026 11:17:38 -0400 Subject: [PATCH 1/8] Pyreason with Python 3.12 --- .github/workflows/perf.yml | 4 +- .../workflows/python-package-version-test.yml | 2 +- README.md | 5 +- docs/source/installation.rst | 2 +- docs/source/tutorials/installation.rst | 2 +- pyreason/__init__.py | 26 ++++---- .../scripts/interpretation/interpretation.py | 64 +++++++++++++++---- .../interpretation/interpretation_fp.py | 64 +++++++++++++++---- .../interpretation/interpretation_parallel.py | 64 +++++++++++++++---- setup.py | 7 +- 10 files changed, 175 insertions(+), 65 deletions(-) diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml index 34f3fbdd..821e9f3b 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -40,7 +40,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.12" - name: Install package # Editable on purpose. run_bench.py's child prepends the repo root to # PYTHONPATH, so the code under measurement is the checkout either way; @@ -74,7 +74,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.12" - name: Install package # Editable on purpose. run_bench.py's child prepends the repo root to # PYTHONPATH, so the code under measurement is the checkout either way; diff --git a/.github/workflows/python-package-version-test.yml b/.github/workflows/python-package-version-test.yml index 4a0f3791..fbc03903 100644 --- a/.github/workflows/python-package-version-test.yml +++ b/.github/workflows/python-package-version-test.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10"] + python-version: ["3.12"] steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 7f9059aa..92469161 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ -[![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.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-3120/) [![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) @@ -43,7 +42,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 version that is currently supported is `3.12`. ## 4. Bibtex If you used this software in your work please cite our paper diff --git a/docs/source/installation.rst b/docs/source/installation.rst index d3586355..fb4358ad 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -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.12. To install PyReason, you can use pip: .. code:: bash diff --git a/docs/source/tutorials/installation.rst b/docs/source/tutorials/installation.rst index 3b41e3dd..976fbdf2 100644 --- a/docs/source/tutorials/installation.rst +++ b/docs/source/tutorials/installation.rst @@ -13,7 +13,7 @@ Prerequisites .. note:: - Use python version 3.9 or 3.10 . + Use python version 3.12 . Step-by-Step Guide diff --git a/pyreason/__init__.py b/pyreason/__init__.py index d0611ae1..7c03498f 100755 --- a/pyreason/__init__.py +++ b/pyreason/__init__.py @@ -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') @@ -9,20 +10,23 @@ 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: - # package is not installed + # Read the installed package version for callers that inspect pyreason.__version__. + __version__ = version(__name__) +except PackageNotFoundError: + # Editable or source checkouts may not be installed as a distribution yet. pass with open(cache_status_path) as file: cache_status = yaml.safe_load(file) -if not cache_status['initialized']: +# Skip first-import Numba warmup under test runners so collection stays fast. +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') @@ -37,9 +41,7 @@ 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) + # Remember that the first-import warmup finished so later imports skip it. + cache_status['initialized'] = True + with open(cache_status_path, 'w') as file: + yaml.dump(cache_status, file) diff --git a/pyreason/scripts/interpretation/interpretation.py b/pyreason/scripts/interpretation/interpretation.py index d4bda1db..5edacd95 100755 --- a/pyreason/scripts/interpretation/interpretation.py +++ b/pyreason/scripts/interpretation/interpretation.py @@ -469,11 +469,27 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # Delete rules that have been applied from list by adding index to list 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]) + # After applying due node rules this timestep, drop them from the + # pending queue so later fixed-point passes do not fire them again. + # The three lists below are parallel: index i is one pending node + # rule, the edges that rule may add, and optional atom-trace data. + 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: + # Keep this still-pending node-rule firing for a later t or FP step. + rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + if atom_trace: + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) + # Swap the pending node-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_node[:] = rules_to_be_applied_node_new + edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new 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]) + rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new # Edges rules_to_remove_idx.clear() @@ -543,11 +559,27 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # Delete rules that have been applied from list by adding the index to list 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]) + # After applying due edge rules this timestep, drop them from the + # pending queue so later fixed-point passes do not fire them again. + # The three lists below are parallel: index i is one pending edge + # rule, the edges that rule may add, and optional atom-trace data. + 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: + # Keep this still-pending edge-rule firing for a later t or FP step. + rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) + if atom_trace: + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) + # Swap the pending edge-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new + edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new 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]) + rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new # Fixed point if update: @@ -1427,15 +1459,17 @@ 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) + # Expand each grounded Z into candidate incoming edges (source, Z). + 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) + # Expand each grounded Y into candidate outgoing edges (Y, neighbor). + for nn in neighbors[n]: + edge_groundings.append((n, nn)) # Case 4: # We have seen both variables before @@ -1447,8 +1481,10 @@ 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) + # Restrict candidates to edges whose endpoints are already grounded. + for nn in neighbors[n]: + if nn in groundings_clause_var_2_set: + edge_groundings.append((n, nn)) return edge_groundings diff --git a/pyreason/scripts/interpretation/interpretation_fp.py b/pyreason/scripts/interpretation/interpretation_fp.py index 207f9377..314c951f 100755 --- a/pyreason/scripts/interpretation/interpretation_fp.py +++ b/pyreason/scripts/interpretation/interpretation_fp.py @@ -681,11 +681,27 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # Delete rules that have been applied from list by adding index to list 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]) + # After applying due node rules this timestep, drop them from the + # pending queue so later fixed-point passes do not fire them again. + # The three lists below are parallel: index i is one pending node + # rule, the edges that rule may add, and optional atom-trace data. + 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: + # Keep this still-pending node-rule firing for a later t or FP step. + rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + if atom_trace: + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) + # Swap the pending node-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_node[:] = rules_to_be_applied_node_new + edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new 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]) + rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new # Edges rules_to_remove_idx.clear() @@ -765,11 +781,27 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # Delete rules that have been applied from list by adding the index to list 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]) + # After applying due edge rules this timestep, drop them from the + # pending queue so later fixed-point passes do not fire them again. + # The three lists below are parallel: index i is one pending edge + # rule, the edges that rule may add, and optional atom-trace data. + 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: + # Keep this still-pending edge-rule firing for a later t or FP step. + rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) + if atom_trace: + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) + # Swap the pending edge-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new + edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new 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]) + rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new # Check for convergence after each timestep (perfect convergence or convergence specified by user) # Check number of changed interpretations or max bound change @@ -1547,15 +1579,17 @@ 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) + # Expand each grounded Z into candidate incoming edges (source, Z). + 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) + # Expand each grounded Y into candidate outgoing edges (Y, neighbor). + for nn in neighbors[n]: + edge_groundings.append((n, nn)) # Case 4: # We have seen both variables before @@ -1567,8 +1601,10 @@ 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) + # Restrict candidates to edges whose endpoints are already grounded. + for nn in neighbors[n]: + if nn in groundings_clause_var_2_set: + edge_groundings.append((n, nn)) return edge_groundings diff --git a/pyreason/scripts/interpretation/interpretation_parallel.py b/pyreason/scripts/interpretation/interpretation_parallel.py index 32d7b3fc..163ca135 100644 --- a/pyreason/scripts/interpretation/interpretation_parallel.py +++ b/pyreason/scripts/interpretation/interpretation_parallel.py @@ -469,11 +469,27 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # Delete rules that have been applied from list by adding index to list 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]) + # After applying due node rules this timestep, drop them from the + # pending queue so later fixed-point passes do not fire them again. + # The three lists below are parallel: index i is one pending node + # rule, the edges that rule may add, and optional atom-trace data. + 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: + # Keep this still-pending node-rule firing for a later t or FP step. + rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + if atom_trace: + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) + # Swap the pending node-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_node[:] = rules_to_be_applied_node_new + edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new 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]) + rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new # Edges rules_to_remove_idx.clear() @@ -543,11 +559,27 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # Delete rules that have been applied from list by adding the index to list 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]) + # After applying due edge rules this timestep, drop them from the + # pending queue so later fixed-point passes do not fire them again. + # The three lists below are parallel: index i is one pending edge + # rule, the edges that rule may add, and optional atom-trace data. + 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: + # Keep this still-pending edge-rule firing for a later t or FP step. + rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) + if atom_trace: + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) + # Swap the pending edge-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new + edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new 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]) + rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new # Fixed point if update: @@ -1427,15 +1459,17 @@ 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) + # Expand each grounded Z into candidate incoming edges (source, Z). + 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) + # Expand each grounded Y into candidate outgoing edges (Y, neighbor). + for nn in neighbors[n]: + edge_groundings.append((n, nn)) # Case 4: # We have seen both variables before @@ -1447,8 +1481,10 @@ 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) + # Restrict candidates to edges whose endpoints are already grounded. + for nn in neighbors[n]: + if nn in groundings_clause_var_2_set: + edge_groundings.append((n, nn)) return edge_groundings diff --git a/setup.py b/setup.py index 35d71a08..3abc41b9 100644 --- a/setup.py +++ b/setup.py @@ -22,16 +22,17 @@ }, classifiers=[ "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent" ], - python_requires='>3.6', + python_requires='>=3.12,<3.13', install_requires=[ 'networkx', 'pyyaml', 'pandas', - 'numba', - 'numpy', + 'numba==0.59.1', + 'numpy==1.26.4', 'memory_profiler', 'pytest' ], From 4a43ea041d827f7282a17b702c13b111918befef Mon Sep 17 00:00:00 2001 From: BAF1927 Date: Tue, 8 Sep 2026 15:20:36 -0400 Subject: [PATCH 2/8] Attempt to fix Time Delays 1 --- pyreason/.cache_status.yaml | 2 +- .../scripts/interpretation/interpretation.py | 88 +++++++++++------- .../interpretation/interpretation_fp.py | 90 +++++++++++-------- .../interpretation/interpretation_parallel.py | 88 +++++++++++------- 4 files changed, 164 insertions(+), 104 deletions(-) diff --git a/pyreason/.cache_status.yaml b/pyreason/.cache_status.yaml index 32458f5d..71173842 100644 --- a/pyreason/.cache_status.yaml +++ b/pyreason/.cache_status.yaml @@ -1 +1 @@ -initialized: false +initialized: true diff --git a/pyreason/scripts/interpretation/interpretation.py b/pyreason/scripts/interpretation/interpretation.py index 5edacd95..5dbb39be 100755 --- a/pyreason/scripts/interpretation/interpretation.py +++ b/pyreason/scripts/interpretation/interpretation.py @@ -473,23 +473,33 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # pending queue so later fixed-point passes do not fire them again. # The three lists below are parallel: index i is one pending node # rule, the edges that rule may add, and optional atom-trace data. - 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: - # Keep this still-pending node-rule firing for a later t or FP step. - rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) - if atom_trace: - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) - # Swap the pending node-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_node[:] = rules_to_be_applied_node_new - edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new - if atom_trace: - rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new + # Skip the rebuild when nothing was applied this pass (common inside + # the fixed-point loop once due rules for t are already drained). + if len(rules_to_remove_idx) > 0: + 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) + if atom_trace: + 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: + # Keep this still-pending node-rule firing for a later t or FP step. + rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) + else: + for keep_idx in range(len(rules_to_be_applied_node)): + if keep_idx not in rules_to_remove_idx: + # Keep this still-pending node-rule firing for a later t or FP step. + rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + # Swap the pending node-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_node[:] = rules_to_be_applied_node_new + edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new + if atom_trace: + rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new # Edges rules_to_remove_idx.clear() @@ -563,23 +573,33 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # pending queue so later fixed-point passes do not fire them again. # The three lists below are parallel: index i is one pending edge # rule, the edges that rule may add, and optional atom-trace data. - 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: - # Keep this still-pending edge-rule firing for a later t or FP step. - rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) - if atom_trace: - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) - # Swap the pending edge-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new - edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new - if atom_trace: - rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new + # Skip the rebuild when nothing was applied this pass (common inside + # the fixed-point loop once due rules for t are already drained). + if len(rules_to_remove_idx) > 0: + 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) + if atom_trace: + 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: + # Keep this still-pending edge-rule firing for a later t or FP step. + rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) + else: + for keep_idx in range(len(rules_to_be_applied_edge)): + if keep_idx not in rules_to_remove_idx: + # Keep this still-pending edge-rule firing for a later t or FP step. + rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) + # Swap the pending edge-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new + edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new + if atom_trace: + rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new # Fixed point if update: diff --git a/pyreason/scripts/interpretation/interpretation_fp.py b/pyreason/scripts/interpretation/interpretation_fp.py index 314c951f..cd174e3b 100755 --- a/pyreason/scripts/interpretation/interpretation_fp.py +++ b/pyreason/scripts/interpretation/interpretation_fp.py @@ -685,23 +685,33 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # pending queue so later fixed-point passes do not fire them again. # The three lists below are parallel: index i is one pending node # rule, the edges that rule may add, and optional atom-trace data. - 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: - # Keep this still-pending node-rule firing for a later t or FP step. - rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) - if atom_trace: - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) - # Swap the pending node-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_node[:] = rules_to_be_applied_node_new - edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new - if atom_trace: - rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new + # Skip the rebuild when nothing was applied this pass (common inside + # the fixed-point loop once due rules for t are already drained). + if len(rules_to_remove_idx) > 0: + 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) + if atom_trace: + 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: + # Keep this still-pending node-rule firing for a later t or FP step. + rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) + else: + for keep_idx in range(len(rules_to_be_applied_node)): + if keep_idx not in rules_to_remove_idx: + # Keep this still-pending node-rule firing for a later t or FP step. + rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + # Swap the pending node-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_node[:] = rules_to_be_applied_node_new + edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new + if atom_trace: + rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new # Edges rules_to_remove_idx.clear() @@ -785,24 +795,34 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # pending queue so later fixed-point passes do not fire them again. # The three lists below are parallel: index i is one pending edge # rule, the edges that rule may add, and optional atom-trace data. - 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: - # Keep this still-pending edge-rule firing for a later t or FP step. - rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) - if atom_trace: - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) - # Swap the pending edge-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new - edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new - if atom_trace: - rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new - + # Skip the rebuild when nothing was applied this pass (common inside + # the fixed-point loop once due rules for t are already drained). + if len(rules_to_remove_idx) > 0: + 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) + if atom_trace: + 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: + # Keep this still-pending edge-rule firing for a later t or FP step. + rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) + else: + for keep_idx in range(len(rules_to_be_applied_edge)): + if keep_idx not in rules_to_remove_idx: + # Keep this still-pending edge-rule firing for a later t or FP step. + rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) + # Swap the pending edge-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new + edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new + if atom_trace: + rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new + # Check for convergence after each timestep (perfect convergence or convergence specified by user) # Check number of changed interpretations or max bound change # User specified convergence diff --git a/pyreason/scripts/interpretation/interpretation_parallel.py b/pyreason/scripts/interpretation/interpretation_parallel.py index 163ca135..47075c48 100644 --- a/pyreason/scripts/interpretation/interpretation_parallel.py +++ b/pyreason/scripts/interpretation/interpretation_parallel.py @@ -473,23 +473,33 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # pending queue so later fixed-point passes do not fire them again. # The three lists below are parallel: index i is one pending node # rule, the edges that rule may add, and optional atom-trace data. - 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: - # Keep this still-pending node-rule firing for a later t or FP step. - rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) - if atom_trace: - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) - # Swap the pending node-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_node[:] = rules_to_be_applied_node_new - edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new - if atom_trace: - rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new + # Skip the rebuild when nothing was applied this pass (common inside + # the fixed-point loop once due rules for t are already drained). + if len(rules_to_remove_idx) > 0: + 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) + if atom_trace: + 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: + # Keep this still-pending node-rule firing for a later t or FP step. + rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) + else: + for keep_idx in range(len(rules_to_be_applied_node)): + if keep_idx not in rules_to_remove_idx: + # Keep this still-pending node-rule firing for a later t or FP step. + rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + # Swap the pending node-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_node[:] = rules_to_be_applied_node_new + edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new + if atom_trace: + rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new # Edges rules_to_remove_idx.clear() @@ -563,23 +573,33 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # pending queue so later fixed-point passes do not fire them again. # The three lists below are parallel: index i is one pending edge # rule, the edges that rule may add, and optional atom-trace data. - 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: - # Keep this still-pending edge-rule firing for a later t or FP step. - rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) - if atom_trace: - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) - # Swap the pending edge-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new - edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new - if atom_trace: - rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new + # Skip the rebuild when nothing was applied this pass (common inside + # the fixed-point loop once due rules for t are already drained). + if len(rules_to_remove_idx) > 0: + 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) + if atom_trace: + 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: + # Keep this still-pending edge-rule firing for a later t or FP step. + rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) + else: + for keep_idx in range(len(rules_to_be_applied_edge)): + if keep_idx not in rules_to_remove_idx: + # Keep this still-pending edge-rule firing for a later t or FP step. + rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) + # Swap the pending edge-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new + edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new + if atom_trace: + rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new # Fixed point if update: From 0cdc5a2dbc0d6d10f95065113ce5eb5532f5eedc Mon Sep 17 00:00:00 2001 From: BAF1927 Date: Mon, 14 Sep 2026 12:53:19 -0400 Subject: [PATCH 3/8] Revert "Attempt to fix Time Delays 1" This reverts commit 4a43ea041d827f7282a17b702c13b111918befef. --- pyreason/.cache_status.yaml | 2 +- .../scripts/interpretation/interpretation.py | 88 +++++++----------- .../interpretation/interpretation_fp.py | 90 ++++++++----------- .../interpretation/interpretation_parallel.py | 88 +++++++----------- 4 files changed, 104 insertions(+), 164 deletions(-) diff --git a/pyreason/.cache_status.yaml b/pyreason/.cache_status.yaml index 71173842..32458f5d 100644 --- a/pyreason/.cache_status.yaml +++ b/pyreason/.cache_status.yaml @@ -1 +1 @@ -initialized: true +initialized: false diff --git a/pyreason/scripts/interpretation/interpretation.py b/pyreason/scripts/interpretation/interpretation.py index 5dbb39be..5edacd95 100755 --- a/pyreason/scripts/interpretation/interpretation.py +++ b/pyreason/scripts/interpretation/interpretation.py @@ -473,33 +473,23 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # pending queue so later fixed-point passes do not fire them again. # The three lists below are parallel: index i is one pending node # rule, the edges that rule may add, and optional atom-trace data. - # Skip the rebuild when nothing was applied this pass (common inside - # the fixed-point loop once due rules for t are already drained). - if len(rules_to_remove_idx) > 0: - 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) - if atom_trace: - 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: - # Keep this still-pending node-rule firing for a later t or FP step. - rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) - else: - for keep_idx in range(len(rules_to_be_applied_node)): - if keep_idx not in rules_to_remove_idx: - # Keep this still-pending node-rule firing for a later t or FP step. - rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) - # Swap the pending node-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_node[:] = rules_to_be_applied_node_new - edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new - if atom_trace: - rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new + 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: + # Keep this still-pending node-rule firing for a later t or FP step. + rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + if atom_trace: + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) + # Swap the pending node-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_node[:] = rules_to_be_applied_node_new + edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new + if atom_trace: + rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new # Edges rules_to_remove_idx.clear() @@ -573,33 +563,23 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # pending queue so later fixed-point passes do not fire them again. # The three lists below are parallel: index i is one pending edge # rule, the edges that rule may add, and optional atom-trace data. - # Skip the rebuild when nothing was applied this pass (common inside - # the fixed-point loop once due rules for t are already drained). - if len(rules_to_remove_idx) > 0: - 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) - if atom_trace: - 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: - # Keep this still-pending edge-rule firing for a later t or FP step. - rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) - else: - for keep_idx in range(len(rules_to_be_applied_edge)): - if keep_idx not in rules_to_remove_idx: - # Keep this still-pending edge-rule firing for a later t or FP step. - rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) - # Swap the pending edge-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new - edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new - if atom_trace: - rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new + 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: + # Keep this still-pending edge-rule firing for a later t or FP step. + rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) + if atom_trace: + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) + # Swap the pending edge-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new + edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new + if atom_trace: + rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new # Fixed point if update: diff --git a/pyreason/scripts/interpretation/interpretation_fp.py b/pyreason/scripts/interpretation/interpretation_fp.py index cd174e3b..314c951f 100755 --- a/pyreason/scripts/interpretation/interpretation_fp.py +++ b/pyreason/scripts/interpretation/interpretation_fp.py @@ -685,33 +685,23 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # pending queue so later fixed-point passes do not fire them again. # The three lists below are parallel: index i is one pending node # rule, the edges that rule may add, and optional atom-trace data. - # Skip the rebuild when nothing was applied this pass (common inside - # the fixed-point loop once due rules for t are already drained). - if len(rules_to_remove_idx) > 0: - 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) - if atom_trace: - 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: - # Keep this still-pending node-rule firing for a later t or FP step. - rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) - else: - for keep_idx in range(len(rules_to_be_applied_node)): - if keep_idx not in rules_to_remove_idx: - # Keep this still-pending node-rule firing for a later t or FP step. - rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) - # Swap the pending node-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_node[:] = rules_to_be_applied_node_new - edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new - if atom_trace: - rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new + 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: + # Keep this still-pending node-rule firing for a later t or FP step. + rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + if atom_trace: + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) + # Swap the pending node-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_node[:] = rules_to_be_applied_node_new + edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new + if atom_trace: + rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new # Edges rules_to_remove_idx.clear() @@ -795,34 +785,24 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # pending queue so later fixed-point passes do not fire them again. # The three lists below are parallel: index i is one pending edge # rule, the edges that rule may add, and optional atom-trace data. - # Skip the rebuild when nothing was applied this pass (common inside - # the fixed-point loop once due rules for t are already drained). - if len(rules_to_remove_idx) > 0: - 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) - if atom_trace: - 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: - # Keep this still-pending edge-rule firing for a later t or FP step. - rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) - else: - for keep_idx in range(len(rules_to_be_applied_edge)): - if keep_idx not in rules_to_remove_idx: - # Keep this still-pending edge-rule firing for a later t or FP step. - rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) - # Swap the pending edge-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new - edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new - if atom_trace: - rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new - + 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: + # Keep this still-pending edge-rule firing for a later t or FP step. + rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) + if atom_trace: + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) + # Swap the pending edge-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new + edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new + if atom_trace: + rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new + # Check for convergence after each timestep (perfect convergence or convergence specified by user) # Check number of changed interpretations or max bound change # User specified convergence diff --git a/pyreason/scripts/interpretation/interpretation_parallel.py b/pyreason/scripts/interpretation/interpretation_parallel.py index 47075c48..163ca135 100644 --- a/pyreason/scripts/interpretation/interpretation_parallel.py +++ b/pyreason/scripts/interpretation/interpretation_parallel.py @@ -473,33 +473,23 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # pending queue so later fixed-point passes do not fire them again. # The three lists below are parallel: index i is one pending node # rule, the edges that rule may add, and optional atom-trace data. - # Skip the rebuild when nothing was applied this pass (common inside - # the fixed-point loop once due rules for t are already drained). - if len(rules_to_remove_idx) > 0: - 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) - if atom_trace: - 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: - # Keep this still-pending node-rule firing for a later t or FP step. - rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) - else: - for keep_idx in range(len(rules_to_be_applied_node)): - if keep_idx not in rules_to_remove_idx: - # Keep this still-pending node-rule firing for a later t or FP step. - rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) - # Swap the pending node-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_node[:] = rules_to_be_applied_node_new - edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new - if atom_trace: - rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new + 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: + # Keep this still-pending node-rule firing for a later t or FP step. + rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + if atom_trace: + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) + # Swap the pending node-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_node[:] = rules_to_be_applied_node_new + edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new + if atom_trace: + rules_to_be_applied_node_trace[:] = rules_to_be_applied_node_trace_new # Edges rules_to_remove_idx.clear() @@ -573,33 +563,23 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # pending queue so later fixed-point passes do not fire them again. # The three lists below are parallel: index i is one pending edge # rule, the edges that rule may add, and optional atom-trace data. - # Skip the rebuild when nothing was applied this pass (common inside - # the fixed-point loop once due rules for t are already drained). - if len(rules_to_remove_idx) > 0: - 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) - if atom_trace: - 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: - # Keep this still-pending edge-rule firing for a later t or FP step. - rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) - else: - for keep_idx in range(len(rules_to_be_applied_edge)): - if keep_idx not in rules_to_remove_idx: - # Keep this still-pending edge-rule firing for a later t or FP step. - rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) - # Swap the pending edge-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new - edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new - if atom_trace: - rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new + 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: + # Keep this still-pending edge-rule firing for a later t or FP step. + rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) + # Same index stores the edge endpoints that firing would introduce. + edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) + if atom_trace: + # Same index stores the ground atoms used to explain that firing. + rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) + # Swap the pending edge-rule queues to the filtered, still-aligned copies. + rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new + edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new + if atom_trace: + rules_to_be_applied_edge_trace[:] = rules_to_be_applied_edge_trace_new # Fixed point if update: From b6b5b3aacf887fdf83ba2d4ac329453dfc3c645b Mon Sep 17 00:00:00 2001 From: BAF1927 Date: Mon, 14 Sep 2026 12:53:19 -0400 Subject: [PATCH 4/8] Revert "Pyreason with Python 3.12" This reverts commit 4535531bdb969423e81d3c5cd96697ae6d1ab6eb. --- .github/workflows/perf.yml | 4 +- .../workflows/python-package-version-test.yml | 2 +- README.md | 5 +- docs/source/installation.rst | 2 +- docs/source/tutorials/installation.rst | 2 +- pyreason/__init__.py | 26 ++++---- .../scripts/interpretation/interpretation.py | 64 ++++--------------- .../interpretation/interpretation_fp.py | 64 ++++--------------- .../interpretation/interpretation_parallel.py | 64 ++++--------------- setup.py | 7 +- 10 files changed, 65 insertions(+), 175 deletions(-) diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml index 821e9f3b..34f3fbdd 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -40,7 +40,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.10" - name: Install package # Editable on purpose. run_bench.py's child prepends the repo root to # PYTHONPATH, so the code under measurement is the checkout either way; @@ -74,7 +74,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.10" - name: Install package # Editable on purpose. run_bench.py's child prepends the repo root to # PYTHONPATH, so the code under measurement is the checkout either way; diff --git a/.github/workflows/python-package-version-test.yml b/.github/workflows/python-package-version-test.yml index fbc03903..4a0f3791 100644 --- a/.github/workflows/python-package-version-test.yml +++ b/.github/workflows/python-package-version-test.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.12"] + python-version: ["3.9", "3.10"] steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 92469161..7f9059aa 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -[![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-3120/) +[![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/) [![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) @@ -42,7 +43,7 @@ PyReason can be installed as a python library using ```bash pip install pyreason ``` -The Python version that is currently supported is `3.12`. +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. ## 4. Bibtex If you used this software in your work please cite our paper diff --git a/docs/source/installation.rst b/docs/source/installation.rst index fb4358ad..d3586355 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -1,7 +1,7 @@ Installation ========== -PyReason is currently compatible with Python 3.12. To install PyReason, you can use pip: +PyReason is currently compatible with Python 3.9 and 3.10. To install PyReason, you can use pip: .. code:: bash diff --git a/docs/source/tutorials/installation.rst b/docs/source/tutorials/installation.rst index 976fbdf2..3b41e3dd 100644 --- a/docs/source/tutorials/installation.rst +++ b/docs/source/tutorials/installation.rst @@ -13,7 +13,7 @@ Prerequisites .. note:: - Use python version 3.12 . + Use python version 3.9 or 3.10 . Step-by-Step Guide diff --git a/pyreason/__init__.py b/pyreason/__init__.py index 7c03498f..d0611ae1 100755 --- a/pyreason/__init__.py +++ b/pyreason/__init__.py @@ -1,7 +1,6 @@ # 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') @@ -10,23 +9,20 @@ from pyreason.pyreason import * import yaml -from importlib.metadata import PackageNotFoundError, version +from importlib.metadata import version +from pkg_resources import get_distribution, DistributionNotFound try: - # Read the installed package version for callers that inspect pyreason.__version__. - __version__ = version(__name__) -except PackageNotFoundError: - # Editable or source checkouts may not be installed as a distribution yet. + __version__ = get_distribution(__name__).version +except DistributionNotFound: + # package is not installed pass with open(cache_status_path) as file: cache_status = yaml.safe_load(file) -# Skip first-import Numba warmup under test runners so collection stays fast. -running_tests = 'pytest' in sys.modules or 'unittest' in sys.modules - -if not cache_status['initialized'] and not running_tests: +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') @@ -41,7 +37,9 @@ print('PyReason initialized!') print() - # Remember that the first-import warmup finished so later imports skip it. - cache_status['initialized'] = True - with open(cache_status_path, 'w') as file: - yaml.dump(cache_status, file) + # 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) diff --git a/pyreason/scripts/interpretation/interpretation.py b/pyreason/scripts/interpretation/interpretation.py index 5edacd95..d4bda1db 100755 --- a/pyreason/scripts/interpretation/interpretation.py +++ b/pyreason/scripts/interpretation/interpretation.py @@ -469,27 +469,11 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # Delete rules that have been applied from list by adding index to list rules_to_remove_idx.add(idx) - # After applying due node rules this timestep, drop them from the - # pending queue so later fixed-point passes do not fire them again. - # The three lists below are parallel: index i is one pending node - # rule, the edges that rule may add, and optional atom-trace data. - 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: - # Keep this still-pending node-rule firing for a later t or FP step. - rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) - if atom_trace: - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) - # Swap the pending node-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_node[:] = rules_to_be_applied_node_new - edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new + # 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[:] = rules_to_be_applied_node_trace_new + 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]) # Edges rules_to_remove_idx.clear() @@ -559,27 +543,11 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # Delete rules that have been applied from list by adding the index to list rules_to_remove_idx.add(idx) - # After applying due edge rules this timestep, drop them from the - # pending queue so later fixed-point passes do not fire them again. - # The three lists below are parallel: index i is one pending edge - # rule, the edges that rule may add, and optional atom-trace data. - 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: - # Keep this still-pending edge-rule firing for a later t or FP step. - rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) - if atom_trace: - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) - # Swap the pending edge-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new - edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new + # 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[:] = rules_to_be_applied_edge_trace_new + 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]) # Fixed point if update: @@ -1459,17 +1427,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]: - # Expand each grounded Z into candidate incoming edges (source, Z). - for nn in reverse_neighbors[n]: - edge_groundings.append((nn, n)) + es = numba.typed.List([(nn, n) for nn in reverse_neighbors[n]]) + edge_groundings.extend(es) # 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]: - # Expand each grounded Y into candidate outgoing edges (Y, neighbor). - for nn in neighbors[n]: - edge_groundings.append((n, nn)) + es = numba.typed.List([(n, nn) for nn in neighbors[n]]) + edge_groundings.extend(es) # Case 4: # We have seen both variables before @@ -1481,10 +1447,8 @@ 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]: - # Restrict candidates to edges whose endpoints are already grounded. - for nn in neighbors[n]: - if nn in groundings_clause_var_2_set: - edge_groundings.append((n, nn)) + es = numba.typed.List([(n, nn) for nn in neighbors[n] if nn in groundings_clause_var_2_set]) + edge_groundings.extend(es) return edge_groundings diff --git a/pyreason/scripts/interpretation/interpretation_fp.py b/pyreason/scripts/interpretation/interpretation_fp.py index 314c951f..207f9377 100755 --- a/pyreason/scripts/interpretation/interpretation_fp.py +++ b/pyreason/scripts/interpretation/interpretation_fp.py @@ -681,27 +681,11 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # Delete rules that have been applied from list by adding index to list rules_to_remove_idx.add(idx) - # After applying due node rules this timestep, drop them from the - # pending queue so later fixed-point passes do not fire them again. - # The three lists below are parallel: index i is one pending node - # rule, the edges that rule may add, and optional atom-trace data. - 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: - # Keep this still-pending node-rule firing for a later t or FP step. - rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) - if atom_trace: - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) - # Swap the pending node-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_node[:] = rules_to_be_applied_node_new - edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new + # 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[:] = rules_to_be_applied_node_trace_new + 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]) # Edges rules_to_remove_idx.clear() @@ -781,27 +765,11 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # Delete rules that have been applied from list by adding the index to list rules_to_remove_idx.add(idx) - # After applying due edge rules this timestep, drop them from the - # pending queue so later fixed-point passes do not fire them again. - # The three lists below are parallel: index i is one pending edge - # rule, the edges that rule may add, and optional atom-trace data. - 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: - # Keep this still-pending edge-rule firing for a later t or FP step. - rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) - if atom_trace: - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) - # Swap the pending edge-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new - edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new + # 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[:] = rules_to_be_applied_edge_trace_new + 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]) # Check for convergence after each timestep (perfect convergence or convergence specified by user) # Check number of changed interpretations or max bound change @@ -1579,17 +1547,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]: - # Expand each grounded Z into candidate incoming edges (source, Z). - for nn in reverse_neighbors[n]: - edge_groundings.append((nn, n)) + es = numba.typed.List([(nn, n) for nn in reverse_neighbors[n]]) + edge_groundings.extend(es) # 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]: - # Expand each grounded Y into candidate outgoing edges (Y, neighbor). - for nn in neighbors[n]: - edge_groundings.append((n, nn)) + es = numba.typed.List([(n, nn) for nn in neighbors[n]]) + edge_groundings.extend(es) # Case 4: # We have seen both variables before @@ -1601,10 +1567,8 @@ 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]: - # Restrict candidates to edges whose endpoints are already grounded. - for nn in neighbors[n]: - if nn in groundings_clause_var_2_set: - edge_groundings.append((n, nn)) + es = numba.typed.List([(n, nn) for nn in neighbors[n] if nn in groundings_clause_var_2_set]) + edge_groundings.extend(es) return edge_groundings diff --git a/pyreason/scripts/interpretation/interpretation_parallel.py b/pyreason/scripts/interpretation/interpretation_parallel.py index 163ca135..32d7b3fc 100644 --- a/pyreason/scripts/interpretation/interpretation_parallel.py +++ b/pyreason/scripts/interpretation/interpretation_parallel.py @@ -469,27 +469,11 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # Delete rules that have been applied from list by adding index to list rules_to_remove_idx.add(idx) - # After applying due node rules this timestep, drop them from the - # pending queue so later fixed-point passes do not fire them again. - # The three lists below are parallel: index i is one pending node - # rule, the edges that rule may add, and optional atom-trace data. - 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: - # Keep this still-pending node-rule firing for a later t or FP step. - rules_to_be_applied_node_new.append(rules_to_be_applied_node[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) - if atom_trace: - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[keep_idx]) - # Swap the pending node-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_node[:] = rules_to_be_applied_node_new - edges_to_be_added_node_rule[:] = edges_to_be_added_node_rule_new + # 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[:] = rules_to_be_applied_node_trace_new + 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]) # Edges rules_to_remove_idx.clear() @@ -559,27 +543,11 @@ def reason(interpretations_node, interpretations_edge, predicate_map_node, predi # Delete rules that have been applied from list by adding the index to list rules_to_remove_idx.add(idx) - # After applying due edge rules this timestep, drop them from the - # pending queue so later fixed-point passes do not fire them again. - # The three lists below are parallel: index i is one pending edge - # rule, the edges that rule may add, and optional atom-trace data. - 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: - # Keep this still-pending edge-rule firing for a later t or FP step. - rules_to_be_applied_edge_new.append(rules_to_be_applied_edge[keep_idx]) - # Same index stores the edge endpoints that firing would introduce. - edges_to_be_added_edge_rule_new.append(edges_to_be_added_edge_rule[keep_idx]) - if atom_trace: - # Same index stores the ground atoms used to explain that firing. - rules_to_be_applied_edge_trace_new.append(rules_to_be_applied_edge_trace[keep_idx]) - # Swap the pending edge-rule queues to the filtered, still-aligned copies. - rules_to_be_applied_edge[:] = rules_to_be_applied_edge_new - edges_to_be_added_edge_rule[:] = edges_to_be_added_edge_rule_new + # 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[:] = rules_to_be_applied_edge_trace_new + 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]) # Fixed point if update: @@ -1459,17 +1427,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]: - # Expand each grounded Z into candidate incoming edges (source, Z). - for nn in reverse_neighbors[n]: - edge_groundings.append((nn, n)) + es = numba.typed.List([(nn, n) for nn in reverse_neighbors[n]]) + edge_groundings.extend(es) # 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]: - # Expand each grounded Y into candidate outgoing edges (Y, neighbor). - for nn in neighbors[n]: - edge_groundings.append((n, nn)) + es = numba.typed.List([(n, nn) for nn in neighbors[n]]) + edge_groundings.extend(es) # Case 4: # We have seen both variables before @@ -1481,10 +1447,8 @@ 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]: - # Restrict candidates to edges whose endpoints are already grounded. - for nn in neighbors[n]: - if nn in groundings_clause_var_2_set: - edge_groundings.append((n, nn)) + es = numba.typed.List([(n, nn) for nn in neighbors[n] if nn in groundings_clause_var_2_set]) + edge_groundings.extend(es) return edge_groundings diff --git a/setup.py b/setup.py index 3abc41b9..35d71a08 100644 --- a/setup.py +++ b/setup.py @@ -22,17 +22,16 @@ }, classifiers=[ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.12", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent" ], - python_requires='>=3.12,<3.13', + python_requires='>3.6', install_requires=[ 'networkx', 'pyyaml', 'pandas', - 'numba==0.59.1', - 'numpy==1.26.4', + 'numba', + 'numpy', 'memory_profiler', 'pytest' ], From 45344416524d682599e90417da3d9336276d93b9 Mon Sep 17 00:00:00 2001 From: BAF1927 Date: Mon, 14 Sep 2026 12:55:08 -0400 Subject: [PATCH 5/8] Port Python 3.10-3.13 compatibility from PR #145 onto main Bring forward the upstream 3.11-3.13 packaging, CI, docs, importlib.metadata init path, Numba pending-rule filter helpers, and edge-grounding loop fixes while keeping current main semantics including the clause-satisfaction hoist. --- .../workflows/python-package-version-test.yml | 7 +- .github/workflows/python-publish.yml | 4 +- .readthedocs.yaml | 2 +- README.md | 6 +- docs/source/installation.rst | 4 +- docs/source/tutorials/installation.rst | 8 +-- pyproject.toml | 2 +- pyreason/__init__.py | 21 +++--- .../scripts/interpretation/interpretation.py | 70 +++++++++++++------ .../interpretation/interpretation_fp.py | 66 ++++++++++++----- .../interpretation/interpretation_parallel.py | 70 +++++++++++++------ requirements.txt | 4 +- run_tests.py | 11 +-- setup.py | 10 ++- 14 files changed, 190 insertions(+), 95 deletions(-) diff --git a/.github/workflows/python-package-version-test.yml b/.github/workflows/python-package-version-test.yml index 4a0f3791..f1914b02 100644 --- a/.github/workflows/python-package-version-test.yml +++ b/.github/workflows/python-package-version-test.yml @@ -16,12 +16,12 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10"] + 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 @@ -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: | diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 479faed3..ff4691f7 100755 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -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 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 296c7943..fe758435 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,7 +5,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.9" + python: "3.10" sphinx: configuration: docs/source/conf.py diff --git a/README.md b/README.md index 7f9059aa..dacba33f 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -[![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) @@ -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`, and `3.13`. ## 4. Bibtex If you used this software in your work please cite our paper diff --git a/docs/source/installation.rst b/docs/source/installation.rst index d3586355..314c024b 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -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.10, 3.11, 3.12, and 3.13. To install PyReason, you can use pip: .. code:: bash @@ -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 `_ 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. diff --git a/docs/source/tutorials/installation.rst b/docs/source/tutorials/installation.rst index 3b41e3dd..a8c05c70 100644 --- a/docs/source/tutorials/installation.rst +++ b/docs/source/tutorials/installation.rst @@ -13,7 +13,7 @@ Prerequisites .. note:: - Use python version 3.9 or 3.10 . + Use Python version 3.10, 3.11, 3.12, or 3.13. Step-by-Step Guide @@ -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` diff --git a/pyproject.toml b/pyproject.toml index bf5379f8..0f50a1ff 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ['setuptools>=42'] +requires = ['setuptools>=42', 'setuptools_scm[toml]>=8'] build-backend = 'setuptools.build_meta' [tool.ruff.lint] diff --git a/pyreason/__init__.py b/pyreason/__init__.py index d0611ae1..45249dfb 100755 --- a/pyreason/__init__.py +++ b/pyreason/__init__.py @@ -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') @@ -9,12 +10,11 @@ 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 @@ -22,7 +22,9 @@ 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') @@ -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) diff --git a/pyreason/scripts/interpretation/interpretation.py b/pyreason/scripts/interpretation/interpretation.py index d4bda1db..e459072e 100755 --- a/pyreason/scripts/interpretation/interpretation.py +++ b/pyreason/scripts/interpretation/interpretation.py @@ -56,6 +56,41 @@ 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]) + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + if atom_trace: + rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[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)) @@ -438,9 +473,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 @@ -470,16 +505,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 @@ -544,10 +576,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: @@ -1427,15 +1456,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 @@ -1447,8 +1476,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 diff --git a/pyreason/scripts/interpretation/interpretation_fp.py b/pyreason/scripts/interpretation/interpretation_fp.py index 207f9377..ae30e462 100755 --- a/pyreason/scripts/interpretation/interpretation_fp.py +++ b/pyreason/scripts/interpretation/interpretation_fp.py @@ -60,6 +60,41 @@ 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]) + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + if atom_trace: + rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[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)) @@ -642,8 +677,8 @@ 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): - t, comp, l, bnd, set_static = i[0], i[1], i[2], i[3], i[4] + for idx, pending_rule in enumerate(rules_to_be_applied_node): + t, comp, l, bnd, set_static = pending_rule[0], pending_rule[1], pending_rule[2], pending_rule[3], pending_rule[4] # if node doesn't exist in interpretation, add it if comp not in interpretations_node[t]: @@ -682,15 +717,12 @@ 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): - t, comp, l, bnd, set_static = i[0], i[1], i[2], i[3], i[4] + for idx, pending_rule in enumerate(rules_to_be_applied_edge): + t, comp, l, bnd, set_static = pending_rule[0], 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[t], interpretations_edge[t], predicate_map_edge, t) changes_cnt += changes @@ -766,10 +798,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) # Check for convergence after each timestep (perfect convergence or convergence specified by user) # Check number of changed interpretations or max bound change @@ -1547,15 +1576,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 @@ -1567,8 +1596,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 diff --git a/pyreason/scripts/interpretation/interpretation_parallel.py b/pyreason/scripts/interpretation/interpretation_parallel.py index 32d7b3fc..2d301aee 100644 --- a/pyreason/scripts/interpretation/interpretation_parallel.py +++ b/pyreason/scripts/interpretation/interpretation_parallel.py @@ -56,6 +56,41 @@ 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]) + edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[keep_idx]) + if atom_trace: + rules_to_be_applied_node_trace_new.append(rules_to_be_applied_node_trace[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)) @@ -438,9 +473,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 @@ -470,16 +505,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 @@ -544,10 +576,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: @@ -1427,15 +1456,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 @@ -1447,8 +1476,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 diff --git a/requirements.txt b/requirements.txt index aabec6b4..1b460a62 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ networkx pyyaml pandas -numba==0.59.1 -numpy==1.26.4 +numba>=0.61.0 +numpy>=1.26.4 memory_profiler pytest torch diff --git a/run_tests.py b/run_tests.py index 582949c2..b3870b0f 100755 --- a/run_tests.py +++ b/run_tests.py @@ -124,14 +124,15 @@ def _find_python_command(self) -> str: if os.path.exists(venv_path): return venv_path - # Try different python commands in order of preference - # Prioritize Python 3.9 as PyReason doesn't support 3.13+ + # Try different python commands in order of preference. + # Prefer currently supported PyReason versions. candidates = [ - 'python3.9', - '/usr/bin/python3', # System Python (often 3.9 on macOS) + 'python3.13', + 'python3.12', + 'python3.11', + 'python3.10', 'python3', 'python', - 'python3.11' ] for cmd in candidates: diff --git a/setup.py b/setup.py index 35d71a08..7417c237 100644 --- a/setup.py +++ b/setup.py @@ -22,16 +22,20 @@ }, classifiers=[ "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent" ], - python_requires='>3.6', + python_requires='>=3.10,<3.14', install_requires=[ 'networkx', 'pyyaml', 'pandas', - 'numba', - 'numpy', + 'numba>=0.61.0', + 'numpy>=1.26.4', 'memory_profiler', 'pytest' ], From 3eeb2c9641e94c619fe8e72a14fc42623f5f368e Mon Sep 17 00:00:00 2001 From: Colton Date: Mon, 5 Oct 2026 12:40:46 -0400 Subject: [PATCH 6/8] Fix pending node rule filtering for delayed inference Node rules leave edges_to_be_added_node_rule empty. The compatibility helpers indexed that list for every retained node rule, raising IndexError when a rule was still waiting to fire. This breaks both two-timestep delays and one-timestep delays combined with a zero-timestep rule. Filter the edge list independently in all three engines, restoring the previous behavior while retaining the explicit loops needed by Numba. Keep pending node rules and their optional trace entries aligned. Add compiled helper tests for retaining all, some, or none of the rules, with tracing enabled and disabled. Add serial and parallel API regressions that verify delayed results, immediate results, and the recorded traces. Validation: - Before the fix: 12 helper cases and 4 serial API cases failed. - Python 3.13: 194 JIT-enabled unit tests passed; classifier tests excluded because this environment lacks PyTorch. - Python 3.13: 466 JIT-disabled unit tests passed, 37 skipped. - Python 3.13: all 8 serial and parallel API regressions passed. - Python 3.10: all 18 new helper cases and engine consistency check passed. - Ruff 0.15.22 and git diff --check passed. --- .../scripts/interpretation/interpretation.py | 6 ++- .../interpretation/interpretation_fp.py | 6 ++- .../interpretation/interpretation_parallel.py | 6 ++- tests/functional/test_delayed_node_rules.py | 50 +++++++++++++++++++ .../test_pending_node_rules.py | 41 +++++++++++++++ 5 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 tests/functional/test_delayed_node_rules.py create mode 100644 tests/unit/dont_disable_jit/test_pending_node_rules.py diff --git a/pyreason/scripts/interpretation/interpretation.py b/pyreason/scripts/interpretation/interpretation.py index e459072e..72d0822e 100755 --- a/pyreason/scripts/interpretation/interpretation.py +++ b/pyreason/scripts/interpretation/interpretation.py @@ -65,9 +65,13 @@ def _filter_pending_node_rules(rules_to_be_applied_node, edges_to_be_added_node_ 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]) - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[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: diff --git a/pyreason/scripts/interpretation/interpretation_fp.py b/pyreason/scripts/interpretation/interpretation_fp.py index ae30e462..682bf734 100755 --- a/pyreason/scripts/interpretation/interpretation_fp.py +++ b/pyreason/scripts/interpretation/interpretation_fp.py @@ -69,9 +69,13 @@ def _filter_pending_node_rules(rules_to_be_applied_node, edges_to_be_added_node_ 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]) - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[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: diff --git a/pyreason/scripts/interpretation/interpretation_parallel.py b/pyreason/scripts/interpretation/interpretation_parallel.py index 2d301aee..cb966782 100644 --- a/pyreason/scripts/interpretation/interpretation_parallel.py +++ b/pyreason/scripts/interpretation/interpretation_parallel.py @@ -65,9 +65,13 @@ def _filter_pending_node_rules(rules_to_be_applied_node, edges_to_be_added_node_ 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]) - edges_to_be_added_node_rule_new.append(edges_to_be_added_node_rule[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: diff --git a/tests/functional/test_delayed_node_rules.py b/tests/functional/test_delayed_node_rules.py new file mode 100644 index 00000000..04b7feab --- /dev/null +++ b/tests/functional/test_delayed_node_rules.py @@ -0,0 +1,50 @@ +import networkx as nx +import pyreason as pr +import pytest + + +@pytest.mark.parametrize("mode", ["regular", "parallel"]) +@pytest.mark.parametrize("atom_trace", [False, True]) +@pytest.mark.parametrize( + "delay, immediate_rule", + [(2, False), (1, True)], + ids=["two-timestep-delay", "mixed-zero-and-one-timestep-delays"], +) +def test_pending_node_rules_keep_their_schedule(mode, atom_trace, delay, immediate_rule): + """Pending node rules survive both a timestep and an extra fixed-point pass.""" + pr.reset() + pr.reset_rules() + pr.reset_settings() + pr.settings.verbose = False + pr.settings.parallel_computing = mode == "parallel" + pr.settings.atom_trace = atom_trace + + graph = nx.DiGraph() + graph.add_node("a") + pr.load_graph(graph) + pr.add_fact(pr.Fact("seed(a)", "seed_fact", 0, 3)) + pr.add_rule(pr.Rule(f"result(x) <-{delay} seed(x)", "delayed_rule")) + if immediate_rule: + pr.add_rule(pr.Rule("immediate(x) <-0 seed(x)", "immediate_rule")) + + interpretation = pr.reason(timesteps=3) + frames = pr.filter_and_sort_nodes(interpretation, ["result"]) + assert len(frames) == 4 + for timestep, frame in enumerate(frames): + if timestep < delay: + assert frame.empty + else: + assert frame.to_dict("records") == [{"component": "a", "result": [1, 1]}] + + if immediate_rule: + immediate_frames = pr.filter_and_sort_nodes(interpretation, ["immediate"]) + assert immediate_frames[0].to_dict("records") == [ + {"component": "a", "immediate": [1, 1]} + ] + + if atom_trace: + trace, _ = pr.get_rule_trace(interpretation) + delayed_trace = trace[trace["Label"] == "result"] + assert set(delayed_trace["Time"]) == set(range(delay, 4)) + assert set(delayed_trace["Occurred Due To"]) == {"delayed_rule"} + assert all(nodes == ["a"] for nodes in delayed_trace["Clause-1"]) diff --git a/tests/unit/dont_disable_jit/test_pending_node_rules.py b/tests/unit/dont_disable_jit/test_pending_node_rules.py new file mode 100644 index 00000000..44b84535 --- /dev/null +++ b/tests/unit/dont_disable_jit/test_pending_node_rules.py @@ -0,0 +1,41 @@ +import importlib + +import numba +import pytest + + +@pytest.mark.parametrize("engine", ["interpretation", "interpretation_parallel", "interpretation_fp"]) +@pytest.mark.parametrize("atom_trace", [False, True]) +@pytest.mark.parametrize( + "removed_indices, expected_indices", + [({-1}, [0, 1, 2]), ({1}, [0, 2]), ({0, 1, 2}, [])], + ids=["retain-all", "remove-middle", "remove-all"], +) +def test_filter_pending_node_rules_with_no_edges(engine, atom_trace, removed_indices, expected_indices): + """Retain delayed node rules and their traces without requiring edge entries.""" + module = importlib.import_module(f"pyreason.scripts.interpretation.{engine}") + rules = numba.typed.List.empty_list(module.rules_to_be_applied_node_type) + edges = numba.typed.List.empty_list(module.edges_to_be_added_type) + traces = numba.typed.List.empty_list(module.rules_to_be_applied_trace_type) + + for index in range(3): + node = f"node_{index}" + rules.append((numba.uint16(index + 1), node, module.label.Label("result"), + module.interval.closed(1, 1), False)) + if atom_trace: + qualified_nodes = numba.typed.List.empty_list(module.list_of_nodes) + qualified_nodes.append(numba.typed.List([node])) + qualified_edges = numba.typed.List.empty_list(module.list_of_edges) + traces.append((qualified_nodes, qualified_edges, f"rule_{index}")) + + module._filter_pending_node_rules(rules, edges, traces, removed_indices, atom_trace) + + assert [rule[1] for rule in rules] == [f"node_{index}" for index in expected_indices] + assert len(edges) == 0 + if atom_trace: + assert [trace[2] for trace in traces] == [f"rule_{index}" for index in expected_indices] + assert [list(trace[0][0]) for trace in traces] == [ + [f"node_{index}"] for index in expected_indices + ] + else: + assert len(traces) == 0 From 768d370019fb5ea2e4669a5946ad3a7a75d24e3d Mon Sep 17 00:00:00 2001 From: Colton Date: Fri, 16 Oct 2026 09:12:37 -0400 Subject: [PATCH 7/8] Restore Python 3.9 support and CI coverage --- .github/workflows/python-package-version-test.yml | 2 +- README.md | 3 ++- docs/source/installation.rst | 2 +- docs/source/tutorials/installation.rst | 2 +- requirements.txt | 4 +++- run_tests.py | 1 + setup.py | 7 +++++-- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/python-package-version-test.yml b/.github/workflows/python-package-version-test.yml index f1914b02..b5664555 100644 --- a/.github/workflows/python-package-version-test.yml +++ b/.github/workflows/python-package-version-test.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index dacba33f..bbe98c75 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ +[![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/) @@ -45,7 +46,7 @@ PyReason can be installed as a python library using ```bash pip install pyreason ``` -The Python versions that are currently supported are `3.10`, `3.11`, `3.12`, and `3.13`. +The Python versions that are currently supported are `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 diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 314c024b..e99e5149 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -1,7 +1,7 @@ Installation ========== -PyReason is currently compatible with Python 3.10, 3.11, 3.12, and 3.13. To install PyReason, you can use pip: +PyReason is currently compatible with Python 3.9, 3.10, 3.11, 3.12, and 3.13. To install PyReason, you can use pip: .. code:: bash diff --git a/docs/source/tutorials/installation.rst b/docs/source/tutorials/installation.rst index a8c05c70..d0629626 100644 --- a/docs/source/tutorials/installation.rst +++ b/docs/source/tutorials/installation.rst @@ -13,7 +13,7 @@ Prerequisites .. note:: - Use Python version 3.10, 3.11, 3.12, or 3.13. + Use Python version 3.9, 3.10, 3.11, 3.12, or 3.13. Step-by-Step Guide diff --git a/requirements.txt b/requirements.txt index 1b460a62..6771d870 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,9 @@ networkx pyyaml pandas -numba>=0.61.0 +# Keep the existing Numba version on Python 3.9; 0.61+ requires 3.10+. +numba==0.59.1; python_version < "3.10" +numba>=0.61.0; python_version >= "3.10" numpy>=1.26.4 memory_profiler pytest diff --git a/run_tests.py b/run_tests.py index b3870b0f..a90adc7a 100755 --- a/run_tests.py +++ b/run_tests.py @@ -131,6 +131,7 @@ def _find_python_command(self) -> str: 'python3.12', 'python3.11', 'python3.10', + 'python3.9', 'python3', 'python', ] diff --git a/setup.py b/setup.py index 7417c237..a097beb4 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ }, classifiers=[ "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -29,12 +30,14 @@ "License :: OSI Approved :: BSD License", "Operating System :: OS Independent" ], - python_requires='>=3.10,<3.14', + python_requires='>=3.9,<3.14', install_requires=[ 'networkx', 'pyyaml', 'pandas', - 'numba>=0.61.0', + # Keep the existing Numba version on Python 3.9; 0.61+ requires 3.10+. + 'numba==0.59.1; python_version < "3.10"', + 'numba>=0.61.0; python_version >= "3.10"', 'numpy>=1.26.4', 'memory_profiler', 'pytest' From d7d5a46d95f3240b48707554fc592954804074bd Mon Sep 17 00:00:00 2001 From: Colton Date: Sat, 17 Oct 2026 11:48:30 -0400 Subject: [PATCH 8/8] Restore Python 3.8 support with compatible dependencies --- .github/workflows/python-package-version-test.yml | 2 +- README.md | 3 ++- docs/source/installation.rst | 2 +- docs/source/tutorials/installation.rst | 2 +- requirements.txt | 8 +++++--- run_tests.py | 1 + setup.py | 11 +++++++---- 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.github/workflows/python-package-version-test.yml b/.github/workflows/python-package-version-test.yml index b5664555..1a8f1b2d 100644 --- a/.github/workflows/python-package-version-test.yml +++ b/.github/workflows/python-package-version-test.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index bbe98c75..077417d3 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ +[![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/) @@ -46,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.9`, `3.10`, `3.11`, `3.12`, and `3.13`. +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 diff --git a/docs/source/installation.rst b/docs/source/installation.rst index e99e5149..d6302746 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -1,7 +1,7 @@ Installation ========== -PyReason is currently compatible with Python 3.9, 3.10, 3.11, 3.12, and 3.13. 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 diff --git a/docs/source/tutorials/installation.rst b/docs/source/tutorials/installation.rst index d0629626..4efa6925 100644 --- a/docs/source/tutorials/installation.rst +++ b/docs/source/tutorials/installation.rst @@ -13,7 +13,7 @@ Prerequisites .. note:: - Use Python version 3.9, 3.10, 3.11, 3.12, or 3.13. + Use Python version 3.8, 3.9, 3.10, 3.11, 3.12, or 3.13. Step-by-Step Guide diff --git a/requirements.txt b/requirements.txt index 6771d870..6908dec6 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,12 @@ networkx pyyaml pandas -# Keep the existing Numba version on Python 3.9; 0.61+ requires 3.10+. -numba==0.59.1; python_version < "3.10" +# Use compatible Numba/NumPy versions for each supported Python version. +numba==0.58.1; python_version < "3.9" +numba==0.59.1; python_version >= "3.9" and python_version < "3.10" numba>=0.61.0; python_version >= "3.10" -numpy>=1.26.4 +numpy==1.24.4; python_version < "3.9" +numpy>=1.26.4; python_version >= "3.9" memory_profiler pytest torch diff --git a/run_tests.py b/run_tests.py index a90adc7a..5bd3f623 100755 --- a/run_tests.py +++ b/run_tests.py @@ -132,6 +132,7 @@ def _find_python_command(self) -> str: 'python3.11', 'python3.10', 'python3.9', + 'python3.8', 'python3', 'python', ] diff --git a/setup.py b/setup.py index a097beb4..38b09fe2 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ }, classifiers=[ "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -30,15 +31,17 @@ "License :: OSI Approved :: BSD License", "Operating System :: OS Independent" ], - python_requires='>=3.9,<3.14', + python_requires='>=3.8,<3.14', install_requires=[ 'networkx', 'pyyaml', 'pandas', - # Keep the existing Numba version on Python 3.9; 0.61+ requires 3.10+. - 'numba==0.59.1; python_version < "3.10"', + # Use compatible Numba/NumPy versions for each supported Python version. + 'numba==0.58.1; python_version < "3.9"', + 'numba==0.59.1; python_version >= "3.9" and python_version < "3.10"', 'numba>=0.61.0; python_version >= "3.10"', - 'numpy>=1.26.4', + 'numpy==1.24.4; python_version < "3.9"', + 'numpy>=1.26.4; python_version >= "3.9"', 'memory_profiler', 'pytest' ],