├── .github └── workflows │ ├── python-package-version-test.yml │ └── python-publish.yml ├── .gitignore ├── .numba_config.yaml ├── .readthedocs.yaml ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── advanced_graph.ipynb ├── friends_graph.graphml ├── group-chat-example.md ├── hello-world.md ├── hello-world.py ├── make.bat ├── pyreason_library.md ├── rules.txt └── source │ ├── _static │ ├── css │ │ └── custom.css │ ├── pyreason_logo.jpg │ └── rule_image.png │ ├── _templates │ └── autoapi │ │ ├── index.rst │ │ └── python │ │ ├── attribute.rst │ │ ├── class.rst │ │ ├── data.rst │ │ ├── exception.rst │ │ ├── function.rst │ │ ├── method.rst │ │ ├── module.rst │ │ ├── package.rst │ │ └── property.rst │ ├── about.rst │ ├── api_reference │ └── index.rst │ ├── conf.py │ ├── examples_rst │ ├── advanced_example.rst │ ├── advanced_output_example.rst │ ├── annF_average_example.rst │ ├── annF_linear_combination_example.rst │ ├── basic_example.rst │ ├── custom_threshold_example.rst │ ├── index.rst │ └── infer_edges_example.rst │ ├── index.rst │ ├── installation.rst │ ├── key_concepts.rst │ ├── license.rst │ ├── tutorials │ ├── advanced_graph.png │ ├── advanced_graph.py │ ├── advanced_tutorial.rst │ ├── annotation_function.rst │ ├── basic_graph.png │ ├── basic_tutorial.rst │ ├── creating_rules.rst │ ├── custom_thresholds.rst │ ├── index.rst │ ├── infer_edges.rst │ ├── installation.rst │ ├── rule_image.png │ └── understanding_logic.rst │ └── user_guide │ ├── 1_pyreason_graphs.rst │ ├── 2_pyreason_facts.rst │ ├── 3_pyreason_rules.rst │ ├── 4_pyreason_settings.rst │ ├── 5_inconsistent_predicate_list.rst │ ├── 6_pyreason_output.rst │ ├── 7_jupyter_notebook_usage.rst │ ├── 8_advanced_usage.rst │ ├── 9_machine_learning_integration.rst │ └── index.rst ├── examples ├── advanced_graph_ex.py ├── advanced_output.txt ├── annotation_function_ex.py ├── basic_tutorial_ex.py ├── classifier_integration_ex.py ├── csv outputs │ ├── advanced_rule_trace_edges_20241119-012153.csv │ ├── advanced_rule_trace_nodes_20241119-012153.csv │ ├── basic_rule_trace_nodes_20241119-012005.csv │ ├── basic_rule_trace_nodes_20241125-114246.csv │ ├── infer_edges_rule_trace_edges_20241119-140955.csv │ └── infer_edges_rule_trace_nodes_20241119-140955.csv ├── custom_threshold_ex.py ├── infer_edges_ex.py ├── temporal_classifier_integration_ex.py └── text.py ├── initialize.py ├── jobs └── .gitignore ├── lit └── README.md ├── media ├── group_chat_graph.png ├── hello_world_friends_graph.png ├── infer_edges1.png ├── infer_edges11.png ├── infer_edges2.png └── pyreason_logo.jpg ├── output └── .gitignore ├── profiling └── .gitignore ├── pyproject.toml ├── pyreason ├── .cache_status.yaml ├── __init__.py ├── examples │ └── hello-world │ │ └── friends_graph.graphml ├── pyreason.py └── scripts │ ├── __init__.py │ ├── annotation_functions │ ├── __init__.py │ └── annotation_functions.py │ ├── components │ ├── __init__.py │ ├── label.py │ └── world.py │ ├── facts │ ├── __init__.py │ ├── fact.py │ ├── fact_edge.py │ └── fact_node.py │ ├── interpretation │ ├── __init__.py │ ├── interpretation.py │ ├── interpretation_dict.py │ ├── interpretation_parallel.py │ └── temp.py │ ├── interval │ ├── __init__.py │ └── interval.py │ ├── learning │ ├── __init__.py │ ├── classification │ │ ├── __init__.py │ │ └── classifier.py │ └── utils │ │ ├── __init__.py │ │ └── model_interface.py │ ├── numba_wrapper │ ├── __init__.py │ └── numba_types │ │ ├── __init__.py │ │ ├── fact_edge_type.py │ │ ├── fact_node_type.py │ │ ├── interval_type.py │ │ ├── label_type.py │ │ ├── rule_type.py │ │ └── world_type.py │ ├── program │ ├── __init__.py │ └── program.py │ ├── query │ ├── __init__.py │ └── query.py │ ├── rules │ ├── __init__.py │ ├── rule.py │ └── rule_internal.py │ ├── threshold │ ├── __init__.py │ └── threshold.py │ └── utils │ ├── __init__.py │ ├── fact_parser.py │ ├── filter.py │ ├── filter_ruleset.py │ ├── graphml_parser.py │ ├── output.py │ ├── plotter.py │ ├── query_parser.py │ ├── reorder_clauses.py │ ├── rule_parser.py │ ├── visuals.py │ └── yaml_parser.py ├── requirements.txt ├── run_on_agave.sh ├── setup.py └── tests ├── friends_graph.graphml ├── group_chat_graph.graphml ├── knowledge_graph_test_subset.graphml ├── test_annotation_function.py ├── test_anyBurl_infer_edges_rules.py ├── test_classifier.py ├── test_custom_thresholds.py ├── test_hello_world.py ├── test_hello_world_parallel.py ├── test_num_ga.py ├── test_reason_again.py ├── test_reorder_clauses.py └── test_rule_filtering.py /.github/workflows/python-package-version-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python version compatibility 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.9", "3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest 31 | pip install torch==2.6.0 32 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 33 | - name: Lint with flake8 34 | run: | 35 | # stop the build if there are Python syntax errors or undefined names 36 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 37 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 38 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 39 | - name: Test with pytest 40 | run: | 41 | pytest 42 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload PYPI Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | cache/ 29 | 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | .idea/ 163 | 164 | *env/ 165 | 166 | # Sphinx Documentation 167 | /docs/source/_static/css/fonts/ 168 | 169 | -------------------------------------------------------------------------------- /.numba_config.yaml: -------------------------------------------------------------------------------- 1 | disable_jit: 0 2 | cache_dir: ./pyreason/cache/ -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | version: 2 4 | 5 | build: 6 | os: ubuntu-22.04 7 | tools: 8 | python: "3.9" 9 | 10 | sphinx: 11 | configuration: docs/source/conf.py 12 | 13 | python: 14 | install: 15 | - requirements: requirements.txt 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2024, Lab V2 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | Trademark Permission 27 | PyReason™ and PyReason Design Logo™ are trademarks of the Arizona Board of Regents/Arizona State University. 28 | Users of the software are permitted to use PyReason™ in association with the software for any purpose, provided such use is related to the software (e.g., Powered by PyReason™). 29 | Additionally, educational institutions are permitted to use the PyReason Design Logo™ for non-commercial purposes. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include pyreason/examples/hello-world/* 2 | include pyreason/.cache_status.yaml 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Python 3.9](https://img.shields.io/badge/python-3.9-blue.svg)](https://www.python.org/downloads/release/python-390/) 4 | [![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg)](https://www.python.org/downloads/release/python-3100/) 5 | [![Documentation Status](https://readthedocs.org/projects/pyreason/badge/?version=latest)](https://pyreason.readthedocs.io/en/latest/?badge=latest) 6 | [![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) 7 | [![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) 8 | 9 | 10 | An explainable inference software supporting annotated, real valued, graph based and temporal logic. 11 | 12 | ## Links 13 | [📃 Paper](https://arxiv.org/abs/2302.13482) 14 | 15 | [📽️ Video](https://www.youtube.com/watch?v=E1PSl3KQCmo) 16 | 17 | [🌐 Website](https://neurosymbolic.asu.edu/pyreason/) 18 | 19 | [🏋️‍♂️ PyReason Gym](https://github.com/lab-v2/pyreason-gym) 20 | 21 | [🗎 Documentation](https://pyreason.readthedocs.io/en/latest/) 22 | 23 | 24 | ## Table of Contents 25 | 26 | 1. [Introduction](#1-introduction) 27 | 2. [Documentation](#2-documentation) 28 | 3. [Install](#3-install) 29 | 4. [Bibtex](#4-bibtex) 30 | 5. [License](#5-license) 31 | 6. [Contact](#6-contact) 32 | 33 | 34 | ## 1. Introduction 35 | PyReason is a graphical inference tool that uses a set of logical rules and facts (initial conditions) to reason over graph structures. To get more details, refer to the paper/video/hello-world-example mentioned above. 36 | 37 | ## 2. Documentation 38 | All API documentation and code examples can be found on [ReadTheDocs](https://pyreason.readthedocs.io/en/latest/) 39 | 40 | ## 3. Install 41 | PyReason can be installed as a python library using 42 | 43 | ```bash 44 | pip install pyreason 45 | ``` 46 | 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. 47 | 48 | ## 4. Bibtex 49 | If you used this software in your work please cite our paper 50 | 51 | Bibtex: 52 | ``` 53 | @inproceedings{aditya_pyreason_2023, 54 | title = {{PyReason}: Software for Open World Temporal Logic}, 55 | booktitle = {{AAAI} Spring Symposium}, 56 | author = {Aditya, Dyuman and Mukherji, Kaustuv and Balasubramanian, Srikar and Chaudhary, Abhiraj and Shakarian, Paulo}, 57 | year = {2023}} 58 | ``` 59 | 60 | ## 5. License 61 | This repository is licensed under [BSD-2-Clause](https://github.com/lab-v2/pyreason/blob/main/LICENSE.md). 62 | 63 | Trademark Permission PyReason™ and PyReason Design Logo ™ are trademarks of the Arizona Board of Regents/Arizona State University. Users of the software are permitted to use PyReason™ in association with the software for any purpose, provided such use is related to the software (e.g., Powered by PyReason™). Additionally, educational institutions are permitted to use the PyReason Design Logo ™ for non-commercial purposes. 64 | 65 | 66 | ## 6. Contact 67 | Dyuman Aditya - dyuman.aditya@asu.edu 68 | 69 | Kaustuv Mukherji - kmukher2@asu.edu 70 | 71 | Paulo Shakarian - pshak02@asu.edu 72 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/friends_graph.graphml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 1 13 | 14 | 15 | 1 16 | 17 | 18 | 1 19 | 20 | 21 | 1 22 | 23 | 24 | 1 25 | 26 | 27 | 1 28 | 29 | 30 | 1 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/group-chat-example.md: -------------------------------------------------------------------------------- 1 | # Custom Thresholds Example 2 | 3 | Here is an example that utilizes custom thresholds. 4 | 5 | The following graph represents a network of People and a Text Message in their group chat. 6 | 7 | 8 | In this case, we want to know when a text message has been viewed by all members of the group chat. 9 | 10 | ## Graph 11 | First, lets create the group chat. 12 | 13 | ```python 14 | import networkx as nx 15 | 16 | # Create an empty graph 17 | G = nx.DiGraph() 18 | 19 | # Add nodes 20 | nodes = ["TextMessage", "Zach", "Justin", "Michelle", "Amy"] 21 | G.add_nodes_from(nodes) 22 | 23 | # Add edges with attribute 'HaveAccess' 24 | edges = [ 25 | ("Zach", "TextMessage", {"HaveAccess": 1}), 26 | ("Justin", "TextMessage", {"HaveAccess": 1}), 27 | ("Michelle", "TextMessage", {"HaveAccess": 1}), 28 | ("Amy", "TextMessage", {"HaveAccess": 1}) 29 | ] 30 | G.add_edges_from(edges) 31 | 32 | ``` 33 | 34 | ## Rules and Custom Thresholds 35 | Considering that we only want a text message to be considered viewed by all if it has been viewed by everyone that can view it, we define the rule as follows: 36 | 37 | ```text 38 | ViewedByAll(y) <- HaveAccess(x,y), Viewed(x) 39 | ``` 40 | 41 | The `head` of the rule is `ViewedByAll(x)` and the body is `HaveAccess(x,y), Viewed(y)`. The head and body are separated by an arrow which means the rule will start evaluating from 42 | timestep 0. 43 | 44 | We add the rule into pyreason with: 45 | 46 | ```python 47 | import pyreason as pr 48 | from pyreason import Threshold 49 | 50 | user_defined_thresholds = [ 51 | Threshold("greater_equal", ("number", "total"), 1), 52 | Threshold("greater_equal", ("percent", "total"), 100), 53 | ] 54 | 55 | pr.add_rule(pr.Rule('ViewedByAll(x) <- HaveAccess(x,y), Viewed(y)', 'viewed_by_all_rule', user_defined_thresholds)) 56 | ``` 57 | Where `viewed_by_all_rule` is the name of the rule. This helps to understand which rule/s are fired during reasoning later on. 58 | 59 | The `user_defined_thresholds` are a list of custom thresholds of the format: (quantifier, quantifier_type, thresh) where: 60 | - quantifier can be greater_equal, greater, less_equal, less, equal 61 | - quantifier_type is a tuple where the first element can be either number or percent and the second element can be either total or available 62 | - thresh represents the numerical threshold value to compare against 63 | 64 | The custom thresholds are created corresponding to the two clauses (HaveAccess(x,y) and Viewed(y)) as below: 65 | - ('greater_equal', ('number', 'total'), 1) (there needs to be at least one person who has access to TextMessage for the first clause to be satisfied) 66 | - ('greater_equal', ('percent', 'total'), 100) (100% of people who have access to TextMessage need to view the message for second clause to be satisfied) 67 | 68 | ## Facts 69 | The facts determine the initial conditions of elements in the graph. They can be specified from the graph attributes but in that 70 | case they will be immutable later on. Adding PyReason facts gives us more flexibility. 71 | 72 | In our case we want one person to view the TextMessage in a particular interval of timestep. 73 | For example, we create facts stating: 74 | - Zach and Justin view the TextMessage from at timestep 0 75 | - Michelle views the TextMessage at timestep 1 76 | - Amy views the TextMessage at timestep 2 77 | 78 | We add the facts in PyReason as below: 79 | ```python 80 | import pyreason as pr 81 | 82 | pr.add_fact(pr.Fact("Viewed(Zach)", "seen-fact-zach", 0, 3)) 83 | pr.add_fact(pr.Fact("Viewed(Justin)", "seen-fact-justin", 0, 3)) 84 | pr.add_fact(pr.Fact("Viewed(Michelle)", "seen-fact-michelle", 1, 3)) 85 | pr.add_fact(pr.Fact("Viewed(Amy)", "seen-fact-amy", 2, 3)) 86 | ``` 87 | 88 | This allows us to specify the component that has an initial condition, the initial condition itself in the form of bounds 89 | as well as the start and end time of this condition. 90 | 91 | ## Running PyReason 92 | Find the full code for this example [here](../tests/test_custom_thresholds.py) 93 | 94 | The main line that runs the reasoning in that file is: 95 | ```python 96 | interpretation = pr.reason(timesteps=3) 97 | ``` 98 | This specifies how many timesteps to run for. 99 | 100 | ## Expected Output 101 | After running the python file, the expected output is: 102 | 103 | ``` 104 | TIMESTEP - 0 105 | Empty DataFrame 106 | Columns: [component, ViewedByAll] 107 | Index: [] 108 | 109 | TIMESTEP - 1 110 | Empty DataFrame 111 | Columns: [component, ViewedByAll] 112 | Index: [] 113 | 114 | TIMESTEP - 2 115 | component ViewedByAll 116 | 0 TextMessage [1.0, 1.0] 117 | 118 | TIMESTEP - 3 119 | component ViewedByAll 120 | 0 TextMessage [1.0, 1.0] 121 | 122 | ``` 123 | 124 | 1. For timestep 0, we set `Zach -> Viewed: [1,1]` and `Justin -> Viewed: [1,1]` in the facts 125 | 2. For timestep 1, Michelle views the TextMessage as stated in facts `Michelle -> Viewed: [1,1]` 126 | 3. For timestep 2, since Amy has just viewed the TextMessage, therefore `Amy -> Viewed: [1,1]`. As per the rule, 127 | since all the people have viewed the TextMessage, the message is marked as ViewedByAll. 128 | 129 | 130 | We also output two CSV files detailing all the events that took place during reasoning (one for nodes, one for edges) 131 | -------------------------------------------------------------------------------- /docs/hello-world.md: -------------------------------------------------------------------------------- 1 | # PyReason Hello World! 🚀 2 | 3 | Welcome to PyReason! In this document we outline a simple program that demonstrates some of the capabilities of the software. If this is your first time looking at the software, you're in the right place. 4 | 5 | The following graph represents a network of people and the pets that they own. 6 | 7 | 8 | 9 | 1. Mary is friends with Justin 10 | 2. Mary is friends with John 11 | 3. Justin is friends with John 12 | 13 | And 14 | 1. Mary owns a cat 15 | 2. Justin owns a cat and a dog 16 | 3. John owns a dog 17 | 18 | 19 | Let's assume that a person's popularity (for illustration 😀) is determined by whether they have AT LEAST ONE friend who is popular AND who has the same pet that they do. If this is true, then they are considered popular. 20 | This will be represented as a Rule later on. 21 | 22 | PyReason needs a few things to run: 23 | 1. A Graph (or knowledge base) 24 | 2. Rules (that determine how things can change in the graph in time) 25 | 3. Facts (that specify initial conditions in the graph. This can be specified in the graph or externally like we'll do now) 26 | 27 | ## Graph 28 | Let's look at how to create a graph using Networkx 29 | 30 | ```python 31 | import networkx as nx 32 | 33 | # ================================ CREATE GRAPH==================================== 34 | # Create a Directed graph 35 | g = nx.DiGraph() 36 | 37 | # Add the nodes 38 | g.add_nodes_from(['John', 'Mary', 'Justin']) 39 | g.add_nodes_from(['Dog', 'Cat']) 40 | 41 | # Add the edges and their attributes. When an attribute = x which is <= 1, the annotation 42 | # associated with it will be [x,1]. NOTE: These attributes are immutable 43 | # Friend edges 44 | g.add_edge('Justin', 'Mary', Friends=1) 45 | g.add_edge('John', 'Mary', Friends=1) 46 | g.add_edge('John', 'Justin', Friends=1) 47 | 48 | # Pet edges 49 | g.add_edge('Mary', 'Cat', owns=1) 50 | g.add_edge('Justin', 'Cat', owns=1) 51 | g.add_edge('Justin', 'Dog', owns=1) 52 | g.add_edge('John', 'Dog', owns=1) 53 | ``` 54 | 55 | 56 | ## Rules 57 | Let's look at the PyReason rule format. Every rule has a `head` and a `body`. The `head` determines what will change in the graph if the `body` is true. 58 | In our case, the rule would look like: 59 | 60 | ```text 61 | popular(x) : [1,1] <-1 popular(y) : [1,1] , Friends(x,y) : [1,1] , owns(y,z) : [1,1] , owns(x,z) : [1,1] 62 | ``` 63 | 64 | Since PyReason by default assumes bounds in a rule to be `[1,1]`, we can omit them here and write: 65 | 66 | ```text 67 | popular(x) <-1 popular(y), Friends(x,y), owns(y,z), owns(x,z) 68 | ``` 69 | 70 | The `head` of the rule is `popular(x)` and the body is `popular(y), Friends(x,y), owns(y,z), owns(x,z)`. The head and body are separated by an arrow and the time after which the head 71 | will become true `<-1` in our case this happens after `1` timestep. 72 | 73 | We add the rule into pyreason with: 74 | 75 | ```python 76 | import pyreason as pr 77 | 78 | pr.add_rule('popular(x) <-1 popular(y), Friends(x,y), owns(y,z), owns(x,z)', 'popular_rule') 79 | ``` 80 | Where `popular_rule` is just the name of the rule. This helps understand which rules fired during reasoning later on. 81 | 82 | ## Facts 83 | The facts determine the initial conditions of elements in the graph. They can be specified from the graph attributes but in that 84 | case they will be immutable later on. Adding PyReason facts gives us more flexibility. 85 | 86 | In our case we want to set on of the people in our graph to be `popular` and use PyReason to see how others in the graph are affected by that. 87 | We add a fact in PyReason like so: 88 | ```python 89 | import pyreason as pr 90 | 91 | pr.add_fact(pr.Fact(fact_text='popular(Mary) : true', name='popular_fact', start_time=0, end_time=2)) 92 | ``` 93 | 94 | This allows us to specify the component that has an initial condition, the initial condition itself in the form of bounds 95 | as well as the start and end time of this condition. 96 | 97 | ## Running PyReason 98 | Find the full code for this example [here](hello-world.py) 99 | 100 | The main line that runs the reasoning in that file is: 101 | ```python 102 | interpretation = pr.reason(timesteps=2) 103 | ``` 104 | This specifies how many timesteps to run for. 105 | 106 | ## Expected Output 107 | After running the python file, the expected output is: 108 | 109 | ``` 110 | TIMESTEP - 0 111 | component popular 112 | 0 Mary [1.0,1.0] 113 | 114 | 115 | TIMESTEP - 1 116 | component popular 117 | 0 Mary [1.0,1.0] 118 | 1 Justin [1.0,1.0] 119 | 120 | 121 | TIMESTEP - 2 122 | component popular 123 | 0 Mary [1.0,1.0] 124 | 1 Justin [1.0,1.0] 125 | 2 John [1.0,1.0] 126 | 127 | ``` 128 | 129 | 1. For timestep 0 we set `Mary -> popular: [1,1]` in the facts 130 | 2. For timestep 1, Justin is the only node who has one popular friend (Mary) and who has the same pet as Mary (cat). Therefore `Justin -> popular: [1,1]` 131 | 3. For timestep 2, since Justin has just become popular, John now has one popular friend (Justin) and the same pet as Justin (dog). Therefore `Justin -> popular: [1,1]` 132 | 133 | 134 | We also output two CSV files detailing all the events that took place during reasoning (one for nodes, one for edges) -------------------------------------------------------------------------------- /docs/hello-world.py: -------------------------------------------------------------------------------- 1 | import pyreason as pr 2 | import networkx as nx 3 | 4 | # ================================ CREATE GRAPH==================================== 5 | # Create a Directed graph 6 | g = nx.DiGraph() 7 | 8 | # Add the nodes 9 | g.add_nodes_from(['John', 'Mary', 'Justin']) 10 | g.add_nodes_from(['Dog', 'Cat']) 11 | 12 | # Add the edges and their attributes. When an attribute = x which is <= 1, the annotation 13 | # associated with it will be [x,1]. NOTE: These attributes are immutable 14 | # Friend edges 15 | g.add_edge('Justin', 'Mary', Friends=1) 16 | g.add_edge('John', 'Mary', Friends=1) 17 | g.add_edge('John', 'Justin', Friends=1) 18 | 19 | # Pet edges 20 | g.add_edge('Mary', 'Cat', owns=1) 21 | g.add_edge('Justin', 'Cat', owns=1) 22 | g.add_edge('Justin', 'Dog', owns=1) 23 | g.add_edge('John', 'Dog', owns=1) 24 | 25 | # ================================= RUN PYREASON ==================================== 26 | 27 | # Modify pyreason settings to make verbose and to save the rule trace to a file 28 | pr.settings.verbose = True # Print info to screen 29 | pr.settings.atom_trace = True # This allows us to view all the atoms that have made a certain rule fire 30 | 31 | 32 | # Load all rules and the graph into pyreason 33 | # Someone is "popular" if they have a friend who is popular and they both own the same pet 34 | pr.load_graph(g) 35 | pr.add_rule(pr.Rule('popular(x) <-1 popular(y), Friends(x,y), owns(y,z), owns(x,z)', 'popular_rule')) 36 | pr.add_fact(pr.Fact('popular-fact', 'Mary', 'popular', [1, 1], 0, 2)) 37 | 38 | 39 | # Run the program for two timesteps to see the diffusion take place 40 | interpretation = pr.reason(timesteps=2) 41 | 42 | # Display the changes in the interpretation for each timestep 43 | dataframes = pr.filter_and_sort_nodes(interpretation, ['popular']) 44 | for t, df in enumerate(dataframes): 45 | print(f'TIMESTEP - {t}') 46 | print(df) 47 | print() 48 | 49 | # Save all changes made to the interpretations a file 50 | pr.save_rule_trace(interpretation) 51 | 52 | # Get all interpretations in a dictionary 53 | interpretations_dict = interpretation.get_interpretation_dict() 54 | 55 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/pyreason_library.md: -------------------------------------------------------------------------------- 1 | # PyReason Python Library 2 | pypi project: https://pypi.org/project/pyreason/ 3 | 4 | ## Install 5 | ```bash 6 | pip install pyreason 7 | python 8 | import pyreason 9 | ``` 10 | We import pyreason to initialize it for the first time, this may take a few minutes 11 | 12 | ## Usage 13 | Example: 14 | ```python 15 | import pyreason as pr 16 | 17 | pr.load_graph(some_networkx_graph) 18 | pr.add_rule(rule_written_in_pyreason_format) 19 | pr.add_fact(pr.Fact(...)) 20 | 21 | pr.settings.verbose = True 22 | interpretation = pr.reason() 23 | ``` 24 | 25 | `load_graph` and `add_rules` have to be called before `pr.reason`. Loading of facts and labels is optional but recommended; if they are not loaded the program will use only the information from attributes in the graph. 26 | 27 | `settings` contains several parameters that can be modified by the user 28 | 29 | `interpretation` is the final interpretation after the reasoning is complete. 30 | -------------------------------------------------------------------------------- /docs/rules.txt: -------------------------------------------------------------------------------- 1 | popular(x) <-1 popular(y), Friends(x,y), owns_pet(y,z), owns_pet(x,z) 2 | -------------------------------------------------------------------------------- /docs/source/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | /*@font-face {*/ 2 | /* font-family: "Freight Sans Pro Bold";*/ 3 | /* src: local("./fonts/FreightSansProBlack-Regular.woff");*/ 4 | /*}*/ 5 | 6 | /** {*/ 7 | /* font-family: "Freight Sans Pro Black Regular", sans-serif;*/ 8 | /*}*/ 9 | -------------------------------------------------------------------------------- /docs/source/_static/pyreason_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/docs/source/_static/pyreason_logo.jpg -------------------------------------------------------------------------------- /docs/source/_static/rule_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/docs/source/_static/rule_image.png -------------------------------------------------------------------------------- /docs/source/_templates/autoapi/index.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | This page contains auto-generated API reference documentation [#f1]_. 5 | 6 | .. toctree:: 7 | :titlesonly: 8 | 9 | {% for page in pages %} 10 | {% if page.top_level_object and page.display %} 11 | {{ page.include_path }} 12 | {% endif %} 13 | {% endfor %} 14 | 15 | .. [#f1] Created with `sphinx-autoapi `_ 16 | -------------------------------------------------------------------------------- /docs/source/_templates/autoapi/python/attribute.rst: -------------------------------------------------------------------------------- 1 | {% extends "python/data.rst" %} 2 | -------------------------------------------------------------------------------- /docs/source/_templates/autoapi/python/class.rst: -------------------------------------------------------------------------------- 1 | {% if obj.display %} 2 | .. py:{{ obj.type }}:: {{ obj.short_name }}{% if obj.args %}({{ obj.args }}){% endif %} 3 | 4 | {% for (args, return_annotation) in obj.overloads %} 5 | {{ " " * (obj.type | length) }} {{ obj.short_name }}{% if args %}({{ args }}){% endif %} 6 | 7 | {% endfor %} 8 | 9 | 10 | {% if obj.bases %} 11 | {% if "show-inheritance" in autoapi_options %} 12 | Bases: {% for base in obj.bases %}{{ base|link_objs }}{% if not loop.last %}, {% endif %}{% endfor %} 13 | {% endif %} 14 | 15 | 16 | {% if "show-inheritance-diagram" in autoapi_options and obj.bases != ["object"] %} 17 | .. autoapi-inheritance-diagram:: {{ obj.obj["full_name"] }} 18 | :parts: 1 19 | {% if "private-members" in autoapi_options %} 20 | :private-bases: 21 | {% endif %} 22 | 23 | {% endif %} 24 | {% endif %} 25 | {% if obj.docstring %} 26 | {{ obj.docstring|indent(3) }} 27 | {% endif %} 28 | {% if "inherited-members" in autoapi_options %} 29 | {% set visible_classes = obj.classes|selectattr("display")|list %} 30 | {% else %} 31 | {% set visible_classes = obj.classes|rejectattr("inherited")|selectattr("display")|list %} 32 | {% endif %} 33 | {% for klass in visible_classes %} 34 | {{ klass.render()|indent(3) }} 35 | {% endfor %} 36 | {% if "inherited-members" in autoapi_options %} 37 | {% set visible_properties = obj.properties|selectattr("display")|list %} 38 | {% else %} 39 | {% set visible_properties = obj.properties|rejectattr("inherited")|selectattr("display")|list %} 40 | {% endif %} 41 | {% for property in visible_properties %} 42 | {{ property.render()|indent(3) }} 43 | {% endfor %} 44 | {% if "inherited-members" in autoapi_options %} 45 | {% set visible_attributes = obj.attributes|selectattr("display")|list %} 46 | {% else %} 47 | {% set visible_attributes = obj.attributes|rejectattr("inherited")|selectattr("display")|list %} 48 | {% endif %} 49 | {% for attribute in visible_attributes %} 50 | {{ attribute.render()|indent(3) }} 51 | {% endfor %} 52 | {% if "inherited-members" in autoapi_options %} 53 | {% set visible_methods = obj.methods|selectattr("display")|list %} 54 | {% else %} 55 | {% set visible_methods = obj.methods|rejectattr("inherited")|selectattr("display")|list %} 56 | {% endif %} 57 | {% for method in visible_methods %} 58 | {{ method.render()|indent(3) }} 59 | {% endfor %} 60 | {% endif %} 61 | -------------------------------------------------------------------------------- /docs/source/_templates/autoapi/python/data.rst: -------------------------------------------------------------------------------- 1 | {% if obj.display %} 2 | .. py:{{ obj.type }}:: {{ obj.name }} 3 | {%- if obj.annotation is not none %} 4 | 5 | :type: {%- if obj.annotation %} {{ obj.annotation }}{%- endif %} 6 | 7 | {%- endif %} 8 | 9 | {%- if obj.value is not none %} 10 | 11 | :value: {% if obj.value is string and obj.value.splitlines()|count > 1 -%} 12 | Multiline-String 13 | 14 | .. raw:: html 15 | 16 |
Show Value 17 | 18 | .. code-block:: python 19 | 20 | """{{ obj.value|indent(width=8,blank=true) }}""" 21 | 22 | .. raw:: html 23 | 24 |
25 | 26 | {%- else -%} 27 | {%- if obj.value is string -%} 28 | {{ "%r" % obj.value|string|truncate(100) }} 29 | {%- else -%} 30 | {{ obj.value|string|truncate(100) }} 31 | {%- endif -%} 32 | {%- endif %} 33 | {%- endif %} 34 | 35 | 36 | {{ obj.docstring|indent(3) }} 37 | {% endif %} 38 | -------------------------------------------------------------------------------- /docs/source/_templates/autoapi/python/exception.rst: -------------------------------------------------------------------------------- 1 | {% extends "python/class.rst" %} 2 | -------------------------------------------------------------------------------- /docs/source/_templates/autoapi/python/function.rst: -------------------------------------------------------------------------------- 1 | {% if obj.display %} 2 | .. py:function:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %} 3 | 4 | {% for (args, return_annotation) in obj.overloads %} 5 | {{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %} 6 | 7 | {% endfor %} 8 | {% for property in obj.properties %} 9 | :{{ property }}: 10 | {% endfor %} 11 | 12 | {% if obj.docstring %} 13 | {{ obj.docstring|indent(3) }} 14 | {% endif %} 15 | {% endif %} 16 | -------------------------------------------------------------------------------- /docs/source/_templates/autoapi/python/method.rst: -------------------------------------------------------------------------------- 1 | {%- if obj.display %} 2 | .. py:method:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %} 3 | 4 | {% for (args, return_annotation) in obj.overloads %} 5 | {{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %} 6 | 7 | {% endfor %} 8 | {% if obj.properties %} 9 | {% for property in obj.properties %} 10 | :{{ property }}: 11 | {% endfor %} 12 | 13 | {% else %} 14 | 15 | {% endif %} 16 | {% if obj.docstring %} 17 | {{ obj.docstring|indent(3) }} 18 | {% endif %} 19 | {% endif %} 20 | -------------------------------------------------------------------------------- /docs/source/_templates/autoapi/python/module.rst: -------------------------------------------------------------------------------- 1 | {% if not obj.display %} 2 | :orphan: 3 | 4 | {% endif %} 5 | :py:mod:`{{ obj.name }}` 6 | =========={{ "=" * obj.name|length }} 7 | 8 | .. py:module:: {{ obj.name }} 9 | 10 | {% if obj.docstring %} 11 | .. autoapi-nested-parse:: 12 | 13 | {{ obj.docstring|indent(3) }} 14 | 15 | {% endif %} 16 | 17 | {% block subpackages %} 18 | {% set visible_subpackages = obj.subpackages|selectattr("display")|list %} 19 | {% if visible_subpackages %} 20 | 21 | .. toctree:: 22 | :titlesonly: 23 | :maxdepth: 3 24 | 25 | {% for subpackage in visible_subpackages %} 26 | {{ subpackage.short_name }}/index.rst 27 | {% endfor %} 28 | 29 | 30 | {% endif %} 31 | {% endblock %} 32 | {% block submodules %} 33 | {% set visible_submodules = obj.submodules|selectattr("display")|list %} 34 | {% if visible_submodules %} 35 | 36 | .. toctree:: 37 | :titlesonly: 38 | :maxdepth: 1 39 | 40 | {% for submodule in visible_submodules %} 41 | {{ submodule.short_name }}/index.rst 42 | {% endfor %} 43 | 44 | 45 | {% endif %} 46 | {% endblock %} 47 | {% block content %} 48 | {% if obj.all is not none %} 49 | {% set visible_children = obj.children|selectattr("short_name", "in", obj.all)|list %} 50 | {% elif obj.type is equalto("package") %} 51 | {% set visible_children = obj.children|selectattr("display")|list %} 52 | {% else %} 53 | {% set visible_children = obj.children|selectattr("display")|rejectattr("imported")|list %} 54 | {% endif %} 55 | {% if visible_children %} 56 | {{ obj.type|title }} Contents 57 | {{ "-" * obj.type|length }}--------- 58 | 59 | {% set visible_classes = visible_children|selectattr("type", "equalto", "class")|list %} 60 | {% set visible_functions = visible_children|selectattr("type", "equalto", "function")|list %} 61 | {% set visible_attributes = visible_children|selectattr("type", "equalto", "data")|list %} 62 | {% if "show-module-summary" in autoapi_options and (visible_classes or visible_functions) %} 63 | {% block classes scoped %} 64 | {% if visible_classes %} 65 | Classes 66 | ~~~~~~~ 67 | 68 | .. autoapisummary:: 69 | 70 | {% for klass in visible_classes %} 71 | {{ klass.id }} 72 | {% endfor %} 73 | 74 | 75 | {% endif %} 76 | {% endblock %} 77 | 78 | {% block functions scoped %} 79 | {% if visible_functions %} 80 | Functions 81 | ~~~~~~~~~ 82 | 83 | .. autoapisummary:: 84 | 85 | {% for function in visible_functions %} 86 | {{ function.id }} 87 | {% endfor %} 88 | 89 | 90 | {% endif %} 91 | {% endblock %} 92 | 93 | {% block attributes scoped %} 94 | {% if visible_attributes %} 95 | Attributes 96 | ~~~~~~~~~~ 97 | 98 | .. autoapisummary:: 99 | 100 | {% for attribute in visible_attributes %} 101 | {{ attribute.id }} 102 | {% endfor %} 103 | 104 | 105 | {% endif %} 106 | {% endblock %} 107 | {% endif %} 108 | {% for obj_item in visible_children %} 109 | {{ obj_item.render()|indent(0) }} 110 | {% endfor %} 111 | {% endif %} 112 | {% endblock %} 113 | -------------------------------------------------------------------------------- /docs/source/_templates/autoapi/python/package.rst: -------------------------------------------------------------------------------- 1 | {% extends "python/module.rst" %} 2 | -------------------------------------------------------------------------------- /docs/source/_templates/autoapi/python/property.rst: -------------------------------------------------------------------------------- 1 | {%- if obj.display %} 2 | .. py:property:: {{ obj.short_name }} 3 | {% if obj.annotation %} 4 | :type: {{ obj.annotation }} 5 | {% endif %} 6 | {% if obj.properties %} 7 | {% for property in obj.properties %} 8 | :{{ property }}: 9 | {% endfor %} 10 | {% endif %} 11 | 12 | {% if obj.docstring %} 13 | {{ obj.docstring|indent(3) }} 14 | {% endif %} 15 | {% endif %} 16 | -------------------------------------------------------------------------------- /docs/source/about.rst: -------------------------------------------------------------------------------- 1 | About PyReason 2 | ============== 3 | 4 | **PyReason** is a modern Python-based software framework designed for open-world temporal logic reasoning using generalized annotated logic. It addresses the growing needs of neuro-symbolic reasoning frameworks that incorporate differentiable logics and temporal extensions, allowing inference over finite periods with open-world capabilities. PyReason is particularly suited for reasoning over graphical structures such as knowledge graphs, social networks, and biological networks, offering fully explainable inference processes. 5 | 6 | Key Capabilities 7 | -------------- 8 | 9 | 1. **Graph-Based Reasoning**: PyReason supports direct reasoning over knowledge graphs, a popular representation of symbolic data. Unlike black-box frameworks, PyReason provides full explainability of the reasoning process. 10 | 11 | 2. **Annotated Logic**: It extends classical logic with annotations, supporting various types of logic including fuzzy logic, real-valued intervals, and temporal logic. PyReason's framework goes beyond traditional logic systems like Prolog, allowing for arbitrary functions over reals, enhancing its capability to handle constructs in neuro-symbolic reasoning. 12 | 13 | 3. **Temporal Reasoning**: PyReason includes temporal extensions to handle reasoning over sequences of time points. This feature enables the creation of rules that incorporate temporal dependencies, such as "if condition A, then condition B after a certain number of time steps." 14 | 15 | 4. **Open World Reasoning**: Unlike closed-world assumptions where anything not explicitly stated is false, PyReason considers unknowns as a valid state, making it more flexible and suitable for real-world applications where information may be incomplete. 16 | 17 | 5. **Handling Logical Inconsistencies**: PyReason can detect and resolve inconsistencies in the reasoning process. When inconsistencies are found, it can reset affected interpretations to a state of complete uncertainty, ensuring that the reasoning process remains robust. 18 | 19 | 6. **Scalability and Performance**: PyReason is optimized for scalability, supporting exact deductive inference with memory-efficient implementations. It leverages sparsity in graphical structures and employs predicate-constant type checking to reduce computational complexity. 20 | 21 | 7. **Explainability**: All inference results produced by PyReason are fully explainable, as the software maintains a trace of the inference steps that led to each conclusion. This feature is critical for applications where transparency of the reasoning process is necessary. 22 | 23 | 8. **Integration and Extensibility**: PyReason is implemented in Python and supports integration with other tools and frameworks, making it easy to extend and adapt for specific needs. It can work with popular graph formats like GraphML and is compatible with tools like NetworkX and Neo4j. 24 | 25 | Use Cases 26 | -------------- 27 | 28 | - **Knowledge Graph Reasoning**: PyReason can be used to perform logical inferences over knowledge graphs, aiding in tasks like knowledge completion, entity classification, and relationship extraction. 29 | 30 | - **Temporal Logic Applications**: Its temporal reasoning capabilities are useful in domains requiring time-based analysis, such as monitoring system states over time, or reasoning about events and their sequences. 31 | 32 | - **Social and Biological Network Analysis**: PyReason's support for annotated logic and reasoning over complex network structures makes it suitable for applications in social network analysis, supply chain management, and biological systems modeling. 33 | 34 | PyReason is open-source and available at: `Github - PyReason `_ 35 | 36 | For more detailed information on PyReason’s logical framework, implementation details, and experimental results, refer to the full documentation or visit the project's GitHub repository. 37 | -------------------------------------------------------------------------------- /docs/source/api_reference/index.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | 5 | .. automodule:: pyreason 6 | :members: 7 | :undoc-members: 8 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | import os 6 | import sys 7 | 8 | #sys.path.insert(0, os.path.abspath('../..')) 9 | #sys.path.insert(0, os.path.abspath('pyreason/pyreason.py')) 10 | # Calculate the absolute path to the pyreason directory 11 | project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'pyreason')) 12 | # Add the pyreason directory to sys.path 13 | sys.path.insert(0, project_root) 14 | 15 | 16 | # -- Project information ----------------------------------------------------- 17 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 18 | 19 | project = 'PyReason' 20 | copyright = '2024, LabV2' 21 | author = 'LabV2' 22 | release = '0.1.0' 23 | 24 | # -- General configuration --------------------------------------------------- 25 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 26 | 27 | extensions = ['sphinx.ext.autodoc', 'sphinx_rtd_theme', 'sphinx.ext.autosummary', 'sphinx.ext.doctest', 28 | 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.mathjax', 'sphinx.ext.ifconfig', 29 | 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 'autoapi.extension',] # Just this line 30 | 31 | autosummary_generate = True 32 | #autoapi_template_dir = '_templates/autoapi' 33 | # Ensure autoapi_dirs points to the folder containing pyreason.py 34 | #autoapi_dirs = [project_root] 35 | autoapi_dirs = [os.path.join(project_root)] # Only include the pyreason directory 36 | 37 | #autoapi_dirs = [os.path.join(project_root)] # Include only 'pyreason.pyreason' 38 | #autoapi_dirs = ['../pyreason/pyreason'] 39 | 40 | autoapi_root = 'pyreason' 41 | autoapi_ignore = ['*/scripts/*', '*/examples/*', '*/pyreason.pyreason/*'] 42 | 43 | # Ignore modules in the 'scripts' folder 44 | # autoapi_ignore_modules = ['pyreason.scripts'] 45 | 46 | 47 | autoapi_options = [ 48 | "members", # Include all class members (functions) 49 | "undoc-members", # Include undocumented members 50 | "show-inheritance", # Show inheritance tree for methods/functions 51 | # "private-members", # Include private members (e.g., _method) 52 | ] 53 | 54 | templates_path = ['_templates'] 55 | 56 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 57 | 58 | # -- Options for HTML output ------------------------------------------------- 59 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 60 | 61 | html_theme = 'sphinx_rtd_theme' 62 | html_static_path = ['_static'] 63 | html_css_files = [ 64 | "css/custom.css", 65 | ] 66 | 67 | add_module_names = False 68 | -------------------------------------------------------------------------------- /docs/source/examples_rst/annF_average_example.rst: -------------------------------------------------------------------------------- 1 | 2 | Average Annotation Function Example 3 | ===================================== 4 | 5 | .. code:: python 6 | 7 | # Test if annotation functions work 8 | import pyreason as pr 9 | import numba 10 | import numpy as np 11 | import networkx as nx 12 | 13 | 14 | 15 | 16 | @numba.njit 17 | def avg_ann_fn(annotations, weights): 18 | # annotations contains the bounds of the atoms that were used to ground the rule. It is a nested list that contains a list for each clause 19 | # You can access for example the first grounded atom's bound by doing: annotations[0][0].lower or annotations[0][0].upper 20 | 21 | # We want the normalised sum of the bounds of the grounded atoms 22 | sum_upper_bounds = 0 23 | sum_lower_bounds = 0 24 | num_atoms = 0 25 | for clause in annotations: 26 | for atom in clause: 27 | sum_lower_bounds += atom.lower 28 | sum_upper_bounds += atom.upper 29 | num_atoms += 1 30 | 31 | a = sum_lower_bounds / num_atoms 32 | b = sum_upper_bounds / num_atoms 33 | return a, b 34 | 35 | 36 | 37 | #Annotation function that returns average of both upper and lower bounds 38 | def average_annotation_function(): 39 | # Reset PyReason 40 | pr.reset() 41 | pr.reset_rules() 42 | 43 | pr.settings.allow_ground_rules = True 44 | 45 | pr.add_fact(pr.Fact('P(A) : [0.01, 1]')) 46 | pr.add_fact(pr.Fact('P(B) : [0.2, 1]')) 47 | pr.add_annotation_function(avg_ann_fn) 48 | pr.add_rule(pr.Rule('average_function(A, B):avg_ann_fn <- P(A):[0, 1], P(B):[0, 1]', infer_edges=True)) 49 | 50 | interpretation = pr.reason(timesteps=1) 51 | 52 | dataframes = pr.filter_and_sort_edges(interpretation, ['average_function']) 53 | for t, df in enumerate(dataframes): 54 | print(f'TIMESTEP - {t}') 55 | print(df) 56 | print() 57 | 58 | assert interpretation.query('average_function(A, B) : [0.105, 1]'), 'Average function should be [0.105, 1]' 59 | 60 | average_annotation_function() 61 | 62 | -------------------------------------------------------------------------------- /docs/source/examples_rst/annF_linear_combination_example.rst: -------------------------------------------------------------------------------- 1 | Linear Combination Annotation Function Example 2 | =================================================== 3 | 4 | .. code:: python 5 | 6 | # Test if annotation functions work 7 | import pyreason as pr 8 | import numba 9 | import numpy as np 10 | import networkx as nx 11 | 12 | 13 | 14 | @numba.njit 15 | def map_to_unit_interval(value, lower, upper): 16 | """ 17 | Map a value from the interval [lower, upper] to the interval [0, 1]. 18 | The formula is f(t) = c + ((d - c) / (b - a)) * (t - a), 19 | where a = lower, b = upper, c = 0, and d = 1. 20 | """ 21 | if upper == lower: 22 | return 0 # Avoid division by zero if upper == lower 23 | return (value - lower) / (upper - lower) 24 | 25 | 26 | @numba.njit 27 | def lin_comb_ann_fn(annotations, weights): 28 | sum_lower_comb = 0 29 | sum_upper_comb = 0 30 | num_atoms = 0 31 | constant = 0.2 32 | 33 | # Iterate over the clauses in the rule 34 | for clause in annotations: 35 | for atom in clause: 36 | # Map the atom's lower and upper bounds to the interval [0, 1] 37 | mapped_lower = map_to_unit_interval(atom.lower, 0, 1) 38 | mapped_upper = map_to_unit_interval(atom.upper, 0, 1) 39 | 40 | # Apply the weights to the lower and upper bounds, and accumulate 41 | sum_lower_comb += constant * mapped_lower 42 | sum_upper_comb += constant * mapped_upper 43 | num_atoms += 1 44 | 45 | # Return the weighted linear combination of the lower and upper bounds 46 | return sum_lower_comb, sum_upper_comb 47 | 48 | 49 | 50 | # Function to run the test 51 | def linear_combination_annotation_function(): 52 | 53 | # Reset PyReason before starting the test 54 | pr.reset() 55 | pr.reset_rules() 56 | 57 | pr.settings.allow_ground_rules = True 58 | 59 | 60 | # Add facts (P(A) and P(B) with bounds) 61 | pr.add_fact(pr.Fact('P(A) : [.3, 1]')) 62 | pr.add_fact(pr.Fact('P(B) : [.2, 1]')) 63 | 64 | 65 | # Register the custom annotation function with PyReason 66 | pr.add_annotation_function(lin_comb_ann_fn) 67 | 68 | # Define a rule that uses this linear combination function 69 | pr.add_rule(pr.Rule('linear_combination_function(A, B):lin_comb_ann_fn <- P(A):[0, 1], P(B):[0, 1]', infer_edges=True)) 70 | 71 | # Perform reasoning for 1 timestep 72 | interpretation = pr.reason(timesteps=1) 73 | 74 | # Filter the results for the computed 'linear_combination_function' edges 75 | dataframes = pr.filter_and_sort_edges(interpretation, ['linear_combination_function']) 76 | 77 | # Print the resulting dataframes for each timestep 78 | for t, df in enumerate(dataframes): 79 | print(f'TIMESTEP - {t}') 80 | print(df) 81 | print() 82 | 83 | # Assert that the linear combination function gives the expected result (adjusted for weights) 84 | # Example assertion based on weights and bounds; adjust the expected result based on the weights 85 | assert interpretation.query('linear_combination_function(A, B) : [0.1, 0.4]'), 'Linear combination function should be [0.105, 1]' 86 | 87 | # Run the test function 88 | linear_combination_annotation_function() -------------------------------------------------------------------------------- /docs/source/examples_rst/basic_example.rst: -------------------------------------------------------------------------------- 1 | Basic Example 2 | ============================ 3 | 4 | 5 | .. code:: python 6 | 7 | # Test if the simple hello world program works 8 | import pyreason as pr 9 | import faulthandler 10 | import networkx as nx 11 | 12 | 13 | # Reset PyReason 14 | pr.reset() 15 | pr.reset_rules() 16 | pr.reset_settings() 17 | 18 | 19 | # ================================ CREATE GRAPH==================================== 20 | # Create a Directed graph 21 | g = nx.DiGraph() 22 | 23 | # Add the nodes 24 | g.add_nodes_from(['John', 'Mary', 'Justin']) 25 | g.add_nodes_from(['Dog', 'Cat']) 26 | 27 | # Add the edges and their attributes. When an attribute = x which is <= 1, the annotation 28 | # associated with it will be [x,1]. NOTE: These attributes are immutable 29 | # Friend edges 30 | g.add_edge('Justin', 'Mary', Friends=1) 31 | g.add_edge('John', 'Mary', Friends=1) 32 | g.add_edge('John', 'Justin', Friends=1) 33 | 34 | # Pet edges 35 | g.add_edge('Mary', 'Cat', owns=1) 36 | g.add_edge('Justin', 'Cat', owns=1) 37 | g.add_edge('Justin', 'Dog', owns=1) 38 | g.add_edge('John', 'Dog', owns=1) 39 | 40 | 41 | # Modify pyreason settings to make verbose 42 | pr.settings.verbose = True # Print info to screen 43 | # pr.settings.optimize_rules = False # Disable rule optimization for debugging 44 | 45 | # Load all the files into pyreason 46 | pr.load_graph(g) 47 | pr.add_rule(pr.Rule('popular(x) <-1 popular(y), Friends(x,y), owns(y,z), owns(x,z)', 'popular_rule')) 48 | pr.add_fact(pr.Fact('popular(Mary)', 'popular_fact', 0, 2)) 49 | 50 | # Run the program for two timesteps to see the diffusion take place 51 | faulthandler.enable() 52 | interpretation = pr.reason(timesteps=2) 53 | 54 | # Display the changes in the interpretation for each timestep 55 | dataframes = pr.filter_and_sort_nodes(interpretation, ['popular']) 56 | for t, df in enumerate(dataframes): 57 | print(f'TIMESTEP - {t}') 58 | print(df) 59 | print() 60 | 61 | assert len(dataframes[0]) == 1, 'At t=0 there should be one popular person' 62 | assert len(dataframes[1]) == 2, 'At t=1 there should be two popular people' 63 | assert len(dataframes[2]) == 3, 'At t=2 there should be three popular people' 64 | 65 | # Mary should be popular in all three timesteps 66 | assert 'Mary' in dataframes[0]['component'].values and dataframes[0].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=0 timesteps' 67 | assert 'Mary' in dataframes[1]['component'].values and dataframes[1].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=1 timesteps' 68 | assert 'Mary' in dataframes[2]['component'].values and dataframes[2].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=2 timesteps' 69 | 70 | # Justin should be popular in timesteps 1, 2 71 | assert 'Justin' in dataframes[1]['component'].values and dataframes[1].iloc[1].popular == [1, 1], 'Justin should have popular bounds [1,1] for t=1 timesteps' 72 | assert 'Justin' in dataframes[2]['component'].values and dataframes[2].iloc[2].popular == [1, 1], 'Justin should have popular bounds [1,1] for t=2 timesteps' 73 | 74 | # John should be popular in timestep 3 75 | assert 'John' in dataframes[2]['component'].values and dataframes[2].iloc[1].popular == [1, 1], 'John should have popular bounds [1,1] for t=2 timesteps' 76 | -------------------------------------------------------------------------------- /docs/source/examples_rst/custom_threshold_example.rst: -------------------------------------------------------------------------------- 1 | Custom Threshold Example 2 | ============================ 3 | 4 | 5 | .. code:: python 6 | 7 | # Test if the simple program works with thresholds defined 8 | import pyreason as pr 9 | from pyreason import Threshold 10 | import networkx as nx 11 | 12 | # Reset PyReason 13 | pr.reset() 14 | pr.reset_rules() 15 | 16 | 17 | # Create an empty graph 18 | G = nx.DiGraph() 19 | 20 | # Add nodes 21 | nodes = ["TextMessage", "Zach", "Justin", "Michelle", "Amy"] 22 | G.add_nodes_from(nodes) 23 | 24 | # Add edges with attribute 'HaveAccess' 25 | G.add_edge("Zach", "TextMessage", HaveAccess=1) 26 | G.add_edge("Justin", "TextMessage", HaveAccess=1) 27 | G.add_edge("Michelle", "TextMessage", HaveAccess=1) 28 | G.add_edge("Amy", "TextMessage", HaveAccess=1) 29 | 30 | 31 | 32 | # Modify pyreason settings to make verbose 33 | pr.reset_settings() 34 | pr.settings.verbose = True # Print info to screen 35 | 36 | #load the graph 37 | pr.load_graph(G) 38 | 39 | # add custom thresholds 40 | user_defined_thresholds = [ 41 | Threshold("greater_equal", ("number", "total"), 1), 42 | Threshold("greater_equal", ("percent", "total"), 100), 43 | 44 | ] 45 | 46 | pr.add_rule( 47 | pr.Rule( 48 | "ViewedByAll(y) <- HaveAccess(x,y), Viewed(x)", 49 | "viewed_by_all_rule", 50 | custom_thresholds=user_defined_thresholds, 51 | ) 52 | ) 53 | 54 | pr.add_fact(pr.Fact("Viewed(Zach)", "seen-fact-zach", 0, 3)) 55 | pr.add_fact(pr.Fact("Viewed(Justin)", "seen-fact-justin", 0, 3)) 56 | pr.add_fact(pr.Fact("Viewed(Michelle)", "seen-fact-michelle", 1, 3)) 57 | pr.add_fact(pr.Fact("Viewed(Amy)", "seen-fact-amy", 2, 3)) 58 | 59 | # Run the program for three timesteps to see the diffusion take place 60 | interpretation = pr.reason(timesteps=3) 61 | 62 | # Display the changes in the interpretation for each timestep 63 | dataframes = pr.filter_and_sort_nodes(interpretation, ["ViewedByAll"]) 64 | for t, df in enumerate(dataframes): 65 | print(f"TIMESTEP - {t}") 66 | print(df) 67 | print() 68 | 69 | assert ( 70 | len(dataframes[0]) == 0 71 | ), "At t=0 the TextMessage should not have been ViewedByAll" 72 | assert ( 73 | len(dataframes[2]) == 1 74 | ), "At t=2 the TextMessage should have been ViewedByAll" 75 | 76 | # TextMessage should be ViewedByAll in t=2 77 | assert "TextMessage" in dataframes[2]["component"].values and dataframes[2].iloc[ 78 | 0 79 | ].ViewedByAll == [ 80 | 1, 81 | 1, 82 | ], "TextMessage should have ViewedByAll bounds [1,1] for t=2 timesteps" 83 | -------------------------------------------------------------------------------- /docs/source/examples_rst/index.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ========== 3 | 4 | In this section we outline a series of tutorials that will help you get started with the basics of using the `pyreason` library. 5 | 6 | Examples 7 | -------- 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Examples: 12 | :glob: 13 | 14 | ./* 15 | 16 | -------------------------------------------------------------------------------- /docs/source/examples_rst/infer_edges_example.rst: -------------------------------------------------------------------------------- 1 | Infer Edges Example 2 | ============================ 3 | 4 | 5 | .. code:: python 6 | 7 | import pyreason as pr 8 | 9 | 10 | import networkx as nx 11 | import matplotlib.pyplot as plt 12 | 13 | # Create a directed graph 14 | G = nx.DiGraph() 15 | 16 | # Add nodes with attributes 17 | nodes = [ 18 | ("Amsterdam_Airport_Schiphol", {"Amsterdam_Airport_Schiphol": 1}), 19 | ("Riga_International_Airport", {"Riga_International_Airport": 1}), 20 | ("Chișinău_International_Airport", {"Chișinău_International_Airport": 1}), 21 | ("Yali", {"Yali": 1}), 22 | ("Düsseldorf_Airport", {"Düsseldorf_Airport": 1}), 23 | ("Pobedilovo_Airport", {"Pobedilovo_Airport": 1}), 24 | ("Dubrovnik_Airport", {"Dubrovnik_Airport": 1}), 25 | ("Hévíz-Balaton_Airport", {"Hévíz-Balaton_Airport": 1}), 26 | ("Athens_International_Airport", {"Athens_International_Airport": 1}), 27 | ("Vnukovo_International_Airport", {"Vnukovo_International_Airport": 1}) 28 | ] 29 | 30 | G.add_nodes_from(nodes) 31 | 32 | # Add edges with 'isConnectedTo' attribute 33 | edges = [ 34 | ("Pobedilovo_Airport", "Vnukovo_International_Airport", {"isConnectedTo": 1}), 35 | ("Vnukovo_International_Airport", "Hévíz-Balaton_Airport", {"isConnectedTo": 1}), 36 | ("Düsseldorf_Airport", "Dubrovnik_Airport", {"isConnectedTo": 1}), 37 | ("Dubrovnik_Airport", "Athens_International_Airport", {"isConnectedTo": 1}), 38 | ("Riga_International_Airport", "Amsterdam_Airport_Schiphol", {"isConnectedTo": 1}), 39 | ("Riga_International_Airport", "Düsseldorf_Airport", {"isConnectedTo": 1}), 40 | ("Chișinău_International_Airport", "Riga_International_Airport", {"isConnectedTo": 1}), 41 | ("Amsterdam_Airport_Schiphol", "Yali", {"isConnectedTo": 1}) 42 | ] 43 | 44 | G.add_edges_from(edges) 45 | 46 | 47 | 48 | # Print a drawing of the directed graph 49 | # nx.draw(G, with_labels=True, node_color='lightblue', font_weight='bold', node_size=3000) 50 | # plt.show() 51 | 52 | 53 | 54 | 55 | pr.reset() 56 | pr.reset_rules() 57 | # Modify pyreason settings to make verbose and to save the rule trace to a file 58 | pr.settings.verbose = True 59 | pr.settings.atom_trace = True 60 | pr.settings.memory_profile = False 61 | pr.settings.canonical = True 62 | pr.settings.inconsistency_check = False 63 | pr.settings.static_graph_facts = False 64 | pr.settings.output_to_file = False 65 | pr.settings.store_interpretation_changes = True 66 | pr.settings.save_graph_attributes_to_trace = True 67 | # Load all the files into pyreason 68 | pr.load_graph(G) 69 | pr.add_rule(pr.Rule('isConnectedTo(A, Y) <-1 isConnectedTo(Y, B), Amsterdam_Airport_Schiphol(B), Vnukovo_International_Airport(A)', 'connected_rule_1', infer_edges=True)) 70 | 71 | # Run the program for two timesteps to see the diffusion take place 72 | interpretation = pr.reason(timesteps=1) 73 | # pr.save_rule_trace(interpretation) 74 | 75 | # Display the changes in the interpretation for each timestep 76 | dataframes = pr.filter_and_sort_edges(interpretation, ['isConnectedTo']) 77 | for t, df in enumerate(dataframes): 78 | print(f'TIMESTEP - {t}') 79 | print(df) 80 | print() 81 | assert len(dataframes) == 2, 'Pyreason should run exactly 2 fixpoint operations' 82 | assert len(dataframes[1]) == 1, 'At t=1 there should be only 1 new isConnectedTo atom' 83 | assert ('Vnukovo_International_Airport', 'Riga_International_Airport') in dataframes[1]['component'].values.tolist() and dataframes[1]['isConnectedTo'].iloc[0] == [1, 1], '(Vnukovo_International_Airport, Riga_International_Airport) should have isConnectedTo bounds [1,1] for t=1 timesteps' 84 | 85 | nx.draw(G, with_labels=True, node_color='lightblue', font_weight='bold', node_size=3000) 86 | plt.show() -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Pyreason documentation master file, created by 2 | sphinx-quickstart on Sun Feb 25 20:15:52 2024. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to PyReason Docs! 7 | ==================================== 8 | 9 | .. image:: _static/pyreason_logo.jpg 10 | :alt: PyReason Logo 11 | :align: center 12 | 13 | Introduction 14 | ------------ 15 | Welcome to the documentation for **PyReason**, a powerful, optimized Python tool for Reasoning over Graphs. PyReason supports a variety of Logics such as Propositional, First Order, Annotated. This documentation will guide you through the installation, usage and API. 16 | 17 | .. toctree:: 18 | :maxdepth: 1 19 | :caption: Contents: 20 | 21 | about 22 | installation 23 | key_concepts 24 | user_guide/index 25 | tutorials/index 26 | license 27 | 28 | 29 | 30 | Getting Help 31 | ------------ 32 | If you encounter any issues or have questions, feel free to check our Github, or contact one of the authors (`dyuman.aditya@asu.edu`, `kmukher2@asu.edu`). 33 | 34 | 35 | Citing PyReason 36 | --------------- 37 | If you use PyReason in your research, please cite the following paper: 38 | 39 | .. code-block:: bibtex 40 | 41 | @inproceedings{aditya_pyreason_2023, 42 | title = {{PyReason}: Software for Open World Temporal Logic}, 43 | booktitle = {{AAAI} Spring Symposium}, 44 | author = {Aditya, Dyuman and Mukherji, Kaustuv and Balasubramanian, Srikar and Chaudhary, Abhiraj and Shakarian, Paulo}, 45 | year = {2023}} 46 | 47 | 48 | Indices and tables 49 | ================== 50 | 51 | * :ref:`genindex` 52 | * :ref:`modindex` 53 | * :ref:`search` 54 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ========== 3 | 4 | PyReason is currently compatible with Python 3.9 and 3.10. To install PyReason, you can use pip: 5 | 6 | .. code:: bash 7 | 8 | pip install pyreason 9 | 10 | 11 | Make sure you're using the correct version of Python. You can create a conda environment with the correct version of Python using the following command: 12 | 13 | .. code:: bash 14 | 15 | conda create -n pyreason-env python=3.10 16 | 17 | PyReason uses a JIT compiler called `Numba `_ to speed up the reasoning process. This means that 18 | the first time PyReason is imported it will have to compile certain functions which will result in faster runtimes later on. 19 | You will see a message like this when you import PyReason for the first time: 20 | 21 | .. code:: text 22 | 23 | Imported PyReason for the first time. Initializing caches for faster runtimes ... this will take a minute -------------------------------------------------------------------------------- /docs/source/key_concepts.rst: -------------------------------------------------------------------------------- 1 | Understanding Key Concepts 2 | ========================== 3 | 4 | .. _rule: 5 | Rule 6 | ~~~~ 7 | 8 | A rule is a statement that establishes a relationship between 9 | premises and a conclusion, allowing for the derivation of the 10 | conclusion if the premises are true. Rules are foundational to 11 | logical systems, facilitating the inference process. 12 | 13 | 14 | .. image:: _static/rule_image.png 15 | :align: center 16 | 17 | Every rule has a head and a body. The head determines what will 18 | change in the graph if the body is true. 19 | 20 | .. _fact: 21 | Fact 22 | ~~~~ 23 | 24 | A fact is a statement that is used to store information in the graph. It is a basic unit 25 | of knowledge that is used to derive new information. It can be thought of as an initial condition before reasoning. 26 | Facts are used to initialize the graph and are the starting point for reasoning. 27 | 28 | Annotated atom 29 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 30 | An annotated atom or function in logic, refers to an atomic formula (or a simple predicate) that is augmented with additional 31 | information, such as a certainty factor, a probability, or other annotations that provide context or constraints. 32 | 33 | In PyReason, an annotated atom is represented as a predicate with a bound, which is a list of two values that represent the lower and upper bounds of the predicate. 34 | For example, a predicate ``pred(x,y) : [0.2, 1]`` means that the predicate ``pred(x,y)`` is true with a certainty between 0.2 and 1. 35 | 36 | Interpretation 37 | ~~~~~~~~~~~~~~ 38 | An interpretation is a mapping from the set of atoms to the set of truth values. It is a way of assigning truth values to the atoms in the graph. 39 | 40 | Fixed point operator 41 | ~~~~~~~~~~~~~~~~~~~~ 42 | 43 | In simple terms, a fixed point operator is a function that says if you have a set of atoms, 44 | return that set plus any atoms that can be derived by a single application of a rule in the program. 45 | 46 | 47 | .. _inconsistent_predicate: 48 | Inconsistencies 49 | ~~~~~~~~~~~~~~~ 50 | A logic program is consistent if there exists an interpretation that satisfies the logic program, i.e., makes all the rules true. 51 | If no such interpretation exists, the logic program is inconsistent. 52 | 53 | For example if we have the following two rules: 54 | 55 | .. code-block:: text 56 | 57 | rule-1: grass_wet <- rained, 58 | rule-2: ~grass_wet <- rained, 59 | 60 | This creates an inconsistency because the first rule states that the grass is wet if it rained, while the second rule states that the grass is not wet if it rained. 61 | In PyReason, inconsistencies are detected and resolved to ensure the reasoning process remains robust. In such a case, 62 | the affected interpretations are reset to a state of complete uncertainty i.e ``grass_wet : [0,1]``. 63 | 64 | Inconsistent predicate list 65 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 66 | 67 | An inconsistent predicate list is a list of predicates that are inconsistent with each other. 68 | 69 | For example, consider the following example of two predicates that are inconsistent with each other: 70 | 71 | .. code-block:: text 72 | 73 | sick and healthy 74 | 75 | In this case, the predicates "sick" and "healthy" are inconsistent with each other because they cannot both be true at the same time. 76 | We can model this in PyReason such that when one predicate is has a certain bound ``[l, u]``, the other predicate is given 77 | a bound ``[1-u, 1-l]`` automatically. See :ref:`here ` for more information. 78 | 79 | In this case, if "sick" is true with a bound ``[1, 1]``, then "healthy" is automatically set to ``[0, 0]``. 80 | -------------------------------------------------------------------------------- /docs/source/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ========== 3 | 4 | This repository is licensed under `BSD-2-Clause `_. 5 | 6 | Trademark Permission 7 | -------------------- 8 | .. |logo| image:: _static/pyreason_logo.jpg 9 | :width: 50 10 | 11 | PyReason™ and PyReason Design Logo |logo| ™ are trademarks of the Arizona Board of Regents/Arizona State University. Users of the software are permitted to use PyReason™ in association with the software for any purpose, provided such use is related to the software (e.g., Powered by PyReason™). Additionally, educational institutions are permitted to use the PyReason Design Logo |logo| ™ for non-commercial purposes. 12 | 13 | -------------------------------------------------------------------------------- /docs/source/tutorials/advanced_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/docs/source/tutorials/advanced_graph.png -------------------------------------------------------------------------------- /docs/source/tutorials/basic_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/docs/source/tutorials/basic_graph.png -------------------------------------------------------------------------------- /docs/source/tutorials/creating_rules.rst: -------------------------------------------------------------------------------- 1 | How to Create Rules 2 | =================== 3 | 4 | Introduction 5 | ------------ 6 | 7 | - Rules are a fundamental part of PyReason. They are used to define the 8 | relationships between different entities in the graph. 9 | - In this section we will be looking at creating different types of 10 | rules. 11 | 12 | Creating Rules 13 | -------------- 14 | 15 | Let us take the examples from the advanced tutorial and create rules for them. 16 | In each of the above rule we have the head of the rule as the a node, i.e. there are no edges 17 | 18 | 1. A customer x is popular if he is friends with a popular customer after 1 timestep. 19 | 20 | .. code-block:: python 21 | 22 | pr.add_rule(pr.Rule('popular(x) <-1 popular(y), Friends(x,y)')) 23 | 24 | 2. A customer x has cool car if he owns a car y and the car is of type Car_4. 25 | 26 | .. code-block:: python 27 | 28 | pr.add_rule(pr.Rule('cool_car(x) <-1 owns_car(x,y),Car_4(y)', 'cool_car_rule')) 29 | 30 | In the above example, note that Car_4 is both the node and the attribute of the node. 31 | 32 | 3. A customer x is a car friend of customer y if they both own the same car and they are not the same person. 33 | 34 | .. code-block:: python 35 | 36 | pr.add_rule(pr.Rule("car_friend(x,y) <- owns_car(x,z), owns_car(y,z) , c_id(x) != c_id(y) ", "car_friend_rule")) 37 | 38 | 39 | Important Tips 40 | --------------- 41 | 42 | Some points to note about the writing rules 43 | 44 | 1. The head of the rule is always on the left hand side of the rule. 45 | 2. The body of the rule is always on the right hand side of the rule. 46 | 3. You can include timestep in the rule by using the `<-timestep` body. 47 | 4. You can include multiple bodies in the rule by using the `<-timestep body1, body2, body3`. 48 | 49 | .. note:: 50 | 51 | 5. To compare two nodes, both the nodes should have an attribute in common. 52 | 1. For example , in the below rule , both the customers have an attribute 'c_id' in common which is the customer id. 53 | 2. So, we can compare the customer id of both the customers to check if they are the same person or not. 54 | 55 | .. code-block:: python 56 | 57 | pr.add_rule(pr.Rule("car_friend(x,y) <- owns_car(x,z), owns_car(y,z) , c_id(x) != c_id(y) ", "car_friend_rule")) 58 | 59 | 6. To compare a particular attribute of a node with another node, you need to use the attribute like in Rule 2 above. 60 | -------------------------------------------------------------------------------- /docs/source/tutorials/index.rst: -------------------------------------------------------------------------------- 1 | Tutorials 2 | ========== 3 | 4 | In this section we outline a series of tutorials that will help you get started with the basics of using the `pyreason` library. 5 | 6 | Contents 7 | -------- 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | :glob: 13 | 14 | ./basic_tutorial.rst 15 | ./advanced_tutorial.rst 16 | ./custom_thresholds.rst 17 | ./infer_edges.rst 18 | ./annotation_function.rst 19 | -------------------------------------------------------------------------------- /docs/source/tutorials/installation.rst: -------------------------------------------------------------------------------- 1 | Installing PyReason 2 | =================== 3 | 4 | This guide details the process of installing Pyreason in an isolated 5 | environment using pyenv and pip. It is particularly useful when managing 6 | projects that require different versions of Python. 7 | 8 | Prerequisites 9 | ------------- 10 | 11 | - Familiarity with terminal or command prompt commands. 12 | - Basic knowledge of Python and its package ecosystem. 13 | 14 | .. note:: 15 | 16 | Use python version 3.9 or 3.10 . 17 | 18 | 19 | Step-by-Step Guide 20 | ------------------ 21 | 22 | 1. Install pyenv 23 | - Ensure your system has the necessary dependencies installed. The installation steps vary by operating system: 24 | 25 | - **Linux/Unix/macOS** 26 | .. code-block:: bash 27 | 28 | brew update brew install pyenv 29 | sudo apt-get update sudo apt-get install make build-essential libssl-dev zlib1g-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev python-openssl 30 | git curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer \| bash 31 | 32 | 2. Configure Environment Variables 33 | - Add pyenv to your profile script to automatically use it in your shell 34 | - **For bash users**: 35 | .. code-block:: bash 36 | 37 | echo 'export PATH="$HOME/.pyenv/bin:$PATH"' >> ~/.bash_profile 38 | echo 'eval "$(pyenv init --path)"' >> ~/.bash_profile 39 | echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bash_profile 40 | - For zsh users: 41 | .. code-block:: bash 42 | 43 | echo 'export PATH="$HOME/.pyenv/bin:$PATH"' >> ~/.zshrc 44 | echo 'eval "$(pyenv init --path)"' >> ~/.zshrc 45 | echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.zshrc`` 46 | 47 | - Restart your shell so the path changes take effect. 48 | 49 | 3. Install Python using `pyenv` 50 | 51 | .. code-block:: bash 52 | 53 | pyenv install 3.8 54 | 55 | 4. Create a Virtual Environment 56 | 57 | .. code-block:: bash 58 | 59 | pyenv virtualenv 3.8 pyreason_venv_3.8 60 | 61 | 5. Activate the Virtual Environment 62 | 63 | .. code-block:: bash 64 | 65 | pyenv activate pyreason_venv_3.8 66 | 67 | 6. Install pyreason Using `pip` 68 | 69 | .. code-block:: bash 70 | 71 | pip install pyreason 72 | 73 | 7. Install requirements.txt 74 | 75 | .. code-block:: bash 76 | 77 | pip install -r requirements.txt 78 | 79 | 8. Deactivate the Virtual Environment 80 | 81 | .. code-block:: bash 82 | 83 | pyenv deactivate 84 | -------------------------------------------------------------------------------- /docs/source/tutorials/rule_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/docs/source/tutorials/rule_image.png -------------------------------------------------------------------------------- /docs/source/tutorials/understanding_logic.rst: -------------------------------------------------------------------------------- 1 | Understanding Key Concepts 2 | ========================== 3 | 4 | Rule 5 | ~~~~ 6 | 7 | - A rule is a statement that establishes a relationship between 8 | premises and a conclusion, allowing for the derivation of the 9 | conclusion if the premises are true. Rules are foundational to 10 | logical systems, facilitating the inference process. |rule_image| 11 | - Every rule has a head and a body. The head determines what will 12 | change in the graph if the body is true. 13 | 14 | Fact 15 | ~~~~ 16 | 17 | - A fact is a statement that is true in the graph. It is a basic unit 18 | of knowledge that is used to derive new information. 19 | - Facts are used to initialize the graph and are the starting point for 20 | reasoning. 21 | 22 | Annotated atom / function 23 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 24 | - An annotated atom or function in logic, refers to an atomic formula (or a simple predicate) that is augmented with additional information, such as a certainty factor, a probability, or other annotations that provide context or constraints. 25 | 26 | Interpretation 27 | ~~~~~~~~~~~~~~ 28 | - An interpretation is a mapping from the set of atoms to the set of truth values. It is a way of assigning truth values to the atoms in the graph. 29 | 30 | Fixed point operator 31 | ~~~~~~~~~~~~~~~~~~~~ 32 | 33 | - In simple terms, a fixed point operator is a function that says if you have a set of atoms, 34 | return that set plus any atoms that can be derived by a single application of a rule in the program. 35 | 36 | 37 | Inconsistent predicate list 38 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 39 | 40 | - A logic program is consistent if there exists an interpretation that satisfies the logic program, i.e., makes all the rules true. If no such interpretation exists, the logic program is inconsistent. An inconsistent predicate list is a list of predicates that are inconsistent with each other. 41 | - An example of an inconsistent predicate list is: 42 | 43 | .. code-block:: 44 | 45 | r1: grass_wet <- rained, 46 | r2: ~ grass_wet <- rained, 47 | f1: rained <- 48 | 49 | - The above example is inconsistent because it contains two rules that are inconsistent with each other. 50 | The first rule states that the grass is wet if it rained, while the second rule states that the grass is not wet if it rained. 51 | The fact f1 states that it rained, which is consistent with the first rule, but inconsistent with the second rule. 52 | 53 | .. |rule_image| image:: rule_image.png 54 | -------------------------------------------------------------------------------- /docs/source/user_guide/2_pyreason_facts.rst: -------------------------------------------------------------------------------- 1 | Facts 2 | ----- 3 | This section outlines Fact creation and implementation. See :ref:`here ` for more information on Facts in logic. 4 | 5 | Fact Parameters 6 | ~~~~~~~~~~~~~~~ 7 | To create a new **Fact** object in PyReason, use the `Fact` class with the following parameters: 8 | 9 | 1. ``fact_text`` **(str):** The fact in text format, where bounds can be specified or not. The bounds are optional. If not specified, the bounds are assumed to be [1,1]. The fact can also be negated using the '~' symbol. 10 | 11 | Examples of valid fact_text are: 12 | 13 | .. code-block:: text 14 | 15 | 1. 'pred(x,y) : [0.2, 1]' 16 | 2. 'pred(x,y)' 17 | 3. '~pred(x,y)' 18 | 19 | 2. ``name`` **(str):** The name of the fact. This will appear in the trace so that you know when it was applied 20 | 3. ``start_time`` **(int):** The timestep at which this fact becomes active (default is 0) 21 | 4. ``end_time`` **(int):** The last timestep this fact is active (default is 0) 22 | 5. ``static`` **(bool):** If the fact should be active for the entire program. In which case ``start_time`` and ``end_time`` will be ignored. (default is False) 23 | 24 | 25 | Fact Example 26 | ~~~~~~~~~~~~ 27 | 28 | To add a fact in PyReason, use the command: 29 | 30 | .. code-block:: python 31 | 32 | import pyreason as pr 33 | pr.add_fact(pr.Fact(fact_text='pred(x,y) : [0.2, 1]', name='fact1', start_time=0, end_time=2)) 34 | -------------------------------------------------------------------------------- /docs/source/user_guide/4_pyreason_settings.rst: -------------------------------------------------------------------------------- 1 | 2 | Settings 3 | ================= 4 | In this section, we detail the settings that can be used to configure PyReason. These settings can be used to control the behavior of the reasoning process. 5 | 6 | Settings can be accessed using the following code: 7 | 8 | .. code-block:: python 9 | 10 | import pyreason as pr 11 | pr.settings.setting_name = value 12 | 13 | Where ``setting_name`` is the name of the setting you want to change, and ``value`` is the value you want to set it to. 14 | Below is a table of all the settings that can be changed in PyReason using the code above. 15 | 16 | .. note:: 17 | All settings need to be modified **before** the reasoning process begins, otherwise they will not take effect. 18 | 19 | To reset all settings to their default values, use the following code: 20 | 21 | .. code-block:: python 22 | 23 | import pyreason as pr 24 | pr.reset_settings() 25 | 26 | 27 | .. list-table:: 28 | 29 | * - **Setting** 30 | - **Default** 31 | - **Description** 32 | * - ``verbose`` 33 | - True 34 | - | Whether to print extra information 35 | | to screen during the reasoning process. 36 | * - ``output_to_file`` 37 | - False 38 | - | Whether to output print statements 39 | | into a file. 40 | * - ``output_file_name`` 41 | - 'pyreason_output' 42 | - | The name the file output will be saved as 43 | | (only if ``output_to_file = True``). 44 | * - ``graph_attribute_parsing`` 45 | - True 46 | - | Whether graph will be 47 | | parsed for attributes. 48 | * - ``reverse_digraph`` 49 | - False 50 | - | Whether the directed edges in the graph 51 | | will be reversed before reasoning. 52 | * - ``atom_trace`` 53 | - False 54 | - | Whether to keep track of all ground atoms 55 | | which make the clauses true. **NOTE:** For large graphs 56 | | this can use up a lot of memory and slow down the runtime. 57 | * - ``save_graph_attributes_to_trace`` 58 | - False 59 | - | Whether to save graph attribute facts to the 60 | | rule trace. This might make the trace files large because 61 | | there are generally many attributes in graphs. 62 | * - ``persistent`` 63 | - False 64 | - | Whether the bounds in the interpretation are reset 65 | | to uncertain ``[0,1]`` at each timestep or keep 66 | | their value from the previous timestep. 67 | * - ``inconsistency_check`` 68 | - True 69 | - | Whether to check for inconsistencies in the interpretation, 70 | | and resolve them if found. Inconsistencies are resolved by 71 | | resetting the bounds to ``[0,1]`` and making the atom static. 72 | * - ``static_graph_facts`` 73 | - True 74 | - | Whether to make graph facts static. In other words, the 75 | | attributes in the graph remain constant throughout 76 | | the reasoning process. 77 | * - ``parallel_computing`` 78 | - False 79 | - | Whether to use multiple CPU cores for inference. 80 | | This can greatly speed up runtime if running on a 81 | | cluster for large graphs. 82 | * - ``update_mode`` 83 | - 'intersection' 84 | - | The mode for updating interpretations. Options are ``'intersection'`` 85 | | or ``'override'``. When using ``'intersection'``, the resulting bound 86 | | is the intersection of the new bound and the old bound. When using 87 | | ``'override'``, the resulting bound is the new bound. 88 | 89 | 90 | Notes on Parallelism 91 | ~~~~~~~~~~~~~~~~~~~~ 92 | PyReason is parallelized over rules, so for large rulesets it is recommended that this setting is used. However, for small rulesets, 93 | the overhead might be more than the speedup and it is worth checking the performance on your specific use case. 94 | When possible we recommend using the same number of cores (or a multiple) as the number of rules in the program. -------------------------------------------------------------------------------- /docs/source/user_guide/5_inconsistent_predicate_list.rst: -------------------------------------------------------------------------------- 1 | .. _inconsistent_predicate_list: 2 | 3 | Inconsistent Predicate List 4 | =========================== 5 | 6 | In this section we detail how we can use inconsistent predicate lists to identify inconsistencies in the graph during reasoning. 7 | For more information on Inconsistencies and the Inconsistent Predicates list, see :ref:`here `. 8 | 9 | For this example, assume we have two inconsistent predicates, "sick" and "healthy". To be able to model this in PyReason 10 | such that when one predicate has a certain bound ``[l, u]``, the other predicate is given a bound ``[1-u, 1-l]`` automatically, 11 | we add the predicates to the **inconsistent predicate list**. 12 | 13 | This can be done by using the following code: 14 | 15 | .. code-block:: python 16 | 17 | import pyreason as pr 18 | pr.add_inconsistent_predicate('sick', 'healthy') 19 | 20 | This allows PyReason to automatically update the bounds of the predicates in the inconsistent predicate list to the 21 | negation of a predicate that is updated. -------------------------------------------------------------------------------- /docs/source/user_guide/7_jupyter_notebook_usage.rst: -------------------------------------------------------------------------------- 1 | Jupyter Notebook Usage 2 | =========================== 3 | 4 | .. warning:: 5 | Using PyReason in a Jupyter Notebook can be a little tricky. And it is recommended to run PyReason in a normal python file. 6 | However, if you want to use PyReason in a Jupyter Notebook, make sure you understand the points below. 7 | 8 | 9 | 1. When using functions like ``add_rule`` or ``add_fact`` in a Jupyter Notebook, make sure to run the cell only once. Running the cell multiple times will add the same rule/fact multiple times. It is recommended to store all the rules and facts in an array and then add them all at once in one cell towards the end 10 | 2. Functions like ``load_graph`` and ``load_graphml`` which are run multiple times can also have the same issue. Make sure to run them only once. 11 | 12 | -------------------------------------------------------------------------------- /docs/source/user_guide/8_advanced_usage.rst: -------------------------------------------------------------------------------- 1 | Advanced Usage of PyReason 2 | =========================== 3 | 4 | PyReason is a powerful tool that can be used to reason over complex systems. This section outlines some advanced usage of PyReason. 5 | 6 | Reasoning Convergence 7 | --------------------- 8 | PyReason uses a fixed point iteration algorithm to reason over the graph. This means that the reasoning process will continue 9 | until the graph reaches a fixed point, i.e., no new facts can be inferred. The fixed point iteration algorithm is guaranteed to converge for acyclic graphs. 10 | However, for cyclic graphs, the algorithm may not converge, and the user may need to set certain values to ensure convergence. 11 | The reasoner contains a few settings that can be used to control the convergence of the reasoning process, and can be set when calling 12 | ``pr.reason(...)`` 13 | 14 | 1. ``convergence_threshold`` **(int, optional)**: The convergence threshold is the maximum number of interpretations that have changed between timesteps or fixed point operations until considered convergent. Program will end at convergence. -1 => no changes, perfect convergence, defaults to -1 15 | 2. ``convergence_bound_threshold`` **(float, optional)**: The convergence bound threshold is the maximum difference between the bounds of the interpretations at each timestep or fixed point operation until considered convergent. Program will end at convergence. -1 => no changes, perfect convergence, defaults to -1 16 | 17 | Reasoning Multiple Times 18 | ------------------------- 19 | PyReason allows you to reason over the graph multiple times. This can be useful when you want to reason over the graph iteratively 20 | and add facts that were not available before. To reason over the graph multiple times, you can set ``again=True`` in ``pr.reason(again=True)``. 21 | To specify additional facts or rules, you can add them as you normally would using ``pr.add_fact`` and ``pr.add_rule``. 22 | 23 | You can also clear the rules to use completely different ones with ``pr.clear_rules()``. This can be useful when you 24 | want to reason over the graph with a new set of rules. 25 | 26 | When reasoning multiple times, the time is reset to zero. Therefore any facts that are added should take this into account. 27 | It is also possible to continue incrementing the time by running ``pr.reason(again=True, restart=False)`` 28 | 29 | .. note:: 30 | When reasoning multiple times with ``restart=False``, the time continues to increment. Therefore any facts that are added should take this into account. 31 | The timestep parameter specifies how many additional timesteps to reason. For example, if the initial reasoning converges at 32 | timestep 5, and you want to reason for 3 more timesteps, you can set ``timestep=3`` in ``pr.reason(timestep=3, again=True)``. 33 | If you are specifying new facts, take this into account when setting their ``start_time`` and ``end_time``. 34 | -------------------------------------------------------------------------------- /docs/source/user_guide/index.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | ========== 3 | 4 | In this section we demonstrate the functionality of the `pyreason` library and how to use it. 5 | 6 | 7 | .. toctree:: 8 | :caption: Contents: 9 | :maxdepth: 2 10 | :glob: 11 | 12 | ./* 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/basic_tutorial_ex.py: -------------------------------------------------------------------------------- 1 | # Test if the simple hello world program works 2 | import pyreason as pr 3 | import faulthandler 4 | import networkx as nx 5 | from typing import Tuple 6 | from pprint import pprint 7 | 8 | 9 | 10 | # Reset PyReason 11 | pr.reset() 12 | pr.reset_rules() 13 | pr.reset_settings() 14 | 15 | 16 | # ================================ CREATE GRAPH==================================== 17 | # Create a Directed graph 18 | g = nx.DiGraph() 19 | 20 | # Add the nodes 21 | g.add_nodes_from(['John', 'Mary', 'Justin']) 22 | g.add_nodes_from(['Dog', 'Cat']) 23 | 24 | # Add the edges and their attributes. When an attribute = x which is <= 1, the annotation 25 | # associated with it will be [x,1]. NOTE: These attributes are immutable 26 | # Friend edges 27 | g.add_edge('Justin', 'Mary', Friends=1) 28 | g.add_edge('John', 'Mary', Friends=1) 29 | g.add_edge('John', 'Justin', Friends=1) 30 | 31 | # Pet edges 32 | g.add_edge('Mary', 'Cat', owns=1) 33 | g.add_edge('Justin', 'Cat', owns=1) 34 | g.add_edge('Justin', 'Dog', owns=1) 35 | g.add_edge('John', 'Dog', owns=1) 36 | 37 | 38 | # Modify pyreason settings to make verbose 39 | pr.settings.verbose = True # Print info to screen 40 | # pr.settings.optimize_rules = False # Disable rule optimization for debugging 41 | 42 | # Load all the files into pyreason 43 | pr.load_graph(g) 44 | pr.add_rule(pr.Rule('popular(x) <-1 popular(y), Friends(x,y), owns(y,z), owns(x,z)', 'popular_rule')) 45 | pr.add_fact(pr.Fact('popular(Mary)', 'popular_fact', 0, 2)) 46 | 47 | # Run the program for two timesteps to see the diffusion take place 48 | faulthandler.enable() 49 | interpretation = pr.reason(timesteps=2) 50 | pr.save_rule_trace(interpretation) 51 | 52 | interpretations_dict = interpretation.get_dict() 53 | print("stra") 54 | pprint(interpretations_dict) 55 | print("end") 56 | #Display the changes in the interpretation for each timestep 57 | dataframes = pr.filter_and_sort_nodes(interpretation, ['popular']) 58 | for t, df in enumerate(dataframes): 59 | print(f'TIMESTEP - {t}') 60 | print(df) 61 | print() 62 | 63 | 64 | 65 | assert len(dataframes[0]) == 1, 'At t=0 there should be one popular person' 66 | assert len(dataframes[1]) == 2, 'At t=1 there should be two popular people' 67 | assert len(dataframes[2]) == 3, 'At t=2 there should be three popular people' 68 | 69 | # Mary should be popular in all three timesteps 70 | assert 'Mary' in dataframes[0]['component'].values and dataframes[0].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=0 timesteps' 71 | assert 'Mary' in dataframes[1]['component'].values and dataframes[1].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=1 timesteps' 72 | assert 'Mary' in dataframes[2]['component'].values and dataframes[2].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=2 timesteps' 73 | 74 | # Justin should be popular in timesteps 1, 2 75 | assert 'Justin' in dataframes[1]['component'].values and dataframes[1].iloc[1].popular == [1, 1], 'Justin should have popular bounds [1,1] for t=1 timesteps' 76 | assert 'Justin' in dataframes[2]['component'].values and dataframes[2].iloc[2].popular == [1, 1], 'Justin should have popular bounds [1,1] for t=2 timesteps' 77 | 78 | # John should be popular in timestep 3 79 | assert 'John' in dataframes[2]['component'].values and dataframes[2].iloc[1].popular == [1, 1], 'John should have popular bounds [1,1] for t=2 timesteps' 80 | -------------------------------------------------------------------------------- /examples/classifier_integration_ex.py: -------------------------------------------------------------------------------- 1 | import pyreason as pr 2 | import torch 3 | import torch.nn as nn 4 | import networkx as nx 5 | import numpy as np 6 | import random 7 | 8 | # seed_value = 41 9 | seed_value = 42 10 | random.seed(seed_value) 11 | np.random.seed(seed_value) 12 | torch.manual_seed(seed_value) 13 | 14 | 15 | # --- Part 1: Fraud Detector Model Integration --- 16 | 17 | # Create a dummy PyTorch model for transaction fraud detection. 18 | # Each transaction is represented by 5 features and is classified into "fraud" or "legitimate". 19 | model = nn.Linear(5, 2) 20 | class_names = ["fraud", "legitimate"] 21 | 22 | # Create a dummy transaction feature vector. 23 | transaction_features = torch.rand(1, 5) 24 | 25 | # Define integration options 26 | # Only probabilities above 0.5 are considered for adjustment. 27 | interface_options = pr.ModelInterfaceOptions( 28 | threshold=0.5, # Only process probabilities above 0.5 29 | set_lower_bound=True, # For high confidence, adjust the lower bound. 30 | set_upper_bound=False, # Keep the upper bound unchanged. 31 | snap_value=1.0 # Use 1.0 as the snap value. 32 | ) 33 | 34 | # Wrap the model using LogicIntegratedClassifier 35 | fraud_detector = pr.LogicIntegratedClassifier( 36 | model, 37 | class_names, 38 | identifier="fraud_detector", 39 | interface_options=interface_options 40 | ) 41 | 42 | # Run the model to obtain logits, probabilities, and generated PyReason facts. 43 | logits, probabilities, classifier_facts = fraud_detector(transaction_features) 44 | 45 | print("=== Fraud Detector Output ===") 46 | print("Logits:", logits) 47 | print("Probabilities:", probabilities) 48 | print("\nGenerated Classifier Facts:") 49 | for fact in classifier_facts: 50 | print(fact) 51 | 52 | # Add the classifier-generated facts. 53 | for fact in classifier_facts: 54 | pr.add_fact(fact) 55 | 56 | # --- Part 2: Create and Load a Networkx Graph representing an account knowledge base --- 57 | 58 | # Create a networkx graph representing a network of accounts. 59 | G = nx.DiGraph() 60 | # Add account nodes. 61 | G.add_node("AccountA", account=1) 62 | G.add_node("AccountB", account=1) 63 | G.add_node("AccountC", account=1) 64 | # Add edges with an attribute "relationship" set to "associated". 65 | G.add_edge("AccountA", "AccountB", associated=1) 66 | G.add_edge("AccountB", "AccountC", associated=1) 67 | pr.load_graph(G) 68 | 69 | # --- Part 3: Set Up Context and Reasoning Environment --- 70 | 71 | # Add additional contextual information: 72 | # 1. A fact indicating the transaction comes from a suspicious location. This could come from a separate fraud detection system. 73 | pr.add_fact(pr.Fact("suspicious_location(AccountA)", "transaction_fact")) 74 | 75 | # Define a rule: if the fraud detector flags a transaction as fraud and the transaction info is suspicious, 76 | # then mark the associated account (AccountA) as requiring investigation. 77 | pr.add_rule(pr.Rule("requires_investigation(acc) <- account(acc), fraud(fraud_detector), suspicious_location(acc)", "investigation_rule")) 78 | 79 | # Define a propagation rule: 80 | # If an account requires investigation and is connected (via the "associated" relationship) to another account, 81 | # then the connected account is also flagged for investigation. 82 | pr.add_rule(pr.Rule("requires_investigation(y) <- requires_investigation(x), associated(x,y)", "propagation_rule")) 83 | 84 | # --- Part 4: Run the Reasoning Engine --- 85 | 86 | # Run the reasoning engine to allow the investigation flag to propagate through the network. 87 | # pr.settings.allow_ground_rules = True 88 | pr.settings.atom_trace = True 89 | interpretation = pr.reason() 90 | 91 | trace = pr.get_rule_trace(interpretation) 92 | print(f"RULE TRACE: \n\n{trace[0]}\n") 93 | -------------------------------------------------------------------------------- /examples/csv outputs/advanced_rule_trace_nodes_20241119-012153.csv: -------------------------------------------------------------------------------- 1 | Time,Fixed-Point-Operation,Node,Label,Old Bound,New Bound,Occurred Due To,Clause-1,Clause-2 2 | 0,0,popular-fac,popular-fac,"[0.0,1.0]","[1.0,1.0]",popular(customer_0),, 3 | 1,2,popular-fac,popular-fac,"[0.0,1.0]","[1.0,1.0]",popular(customer_0),, 4 | 1,2,customer_4,cool_car,"[0.0,1.0]","[1.0,1.0]",cool_car_rule,"[('customer_4', 'Car_4')]",['Car_4'] 5 | 1,2,customer_6,cool_car,"[0.0,1.0]","[1.0,1.0]",cool_car_rule,"[('customer_6', 'Car_4')]",['Car_4'] 6 | 1,2,customer_3,cool_pet,"[0.0,1.0]","[1.0,1.0]",cool_pet_rule,"[('customer_3', 'Pet_2')]",['Pet_2'] 7 | 1,2,customer_4,cool_pet,"[0.0,1.0]","[1.0,1.0]",cool_pet_rule,"[('customer_4', 'Pet_2')]",['Pet_2'] 8 | 1,3,customer_4,trendy,"[0.0,1.0]","[1.0,1.0]",trendy_rule,['customer_4'],['customer_4'] 9 | 2,4,popular-fac,popular-fac,"[0.0,1.0]","[1.0,1.0]",popular(customer_0),, 10 | 2,4,customer_4,cool_car,"[0.0,1.0]","[1.0,1.0]",cool_car_rule,"[('customer_4', 'Car_4')]",['Car_4'] 11 | 2,4,customer_6,cool_car,"[0.0,1.0]","[1.0,1.0]",cool_car_rule,"[('customer_6', 'Car_4')]",['Car_4'] 12 | 2,4,customer_3,cool_pet,"[0.0,1.0]","[1.0,1.0]",cool_pet_rule,"[('customer_3', 'Pet_2')]",['Pet_2'] 13 | 2,4,customer_4,cool_pet,"[0.0,1.0]","[1.0,1.0]",cool_pet_rule,"[('customer_4', 'Pet_2')]",['Pet_2'] 14 | 2,5,customer_4,trendy,"[0.0,1.0]","[1.0,1.0]",trendy_rule,['customer_4'],['customer_4'] 15 | 3,6,popular-fac,popular-fac,"[0.0,1.0]","[1.0,1.0]",popular(customer_0),, 16 | 3,6,customer_4,cool_car,"[0.0,1.0]","[1.0,1.0]",cool_car_rule,"[('customer_4', 'Car_4')]",['Car_4'] 17 | 3,6,customer_6,cool_car,"[0.0,1.0]","[1.0,1.0]",cool_car_rule,"[('customer_6', 'Car_4')]",['Car_4'] 18 | 3,6,customer_3,cool_pet,"[0.0,1.0]","[1.0,1.0]",cool_pet_rule,"[('customer_3', 'Pet_2')]",['Pet_2'] 19 | 3,6,customer_4,cool_pet,"[0.0,1.0]","[1.0,1.0]",cool_pet_rule,"[('customer_4', 'Pet_2')]",['Pet_2'] 20 | 3,7,customer_4,trendy,"[0.0,1.0]","[1.0,1.0]",trendy_rule,['customer_4'],['customer_4'] 21 | 4,8,popular-fac,popular-fac,"[0.0,1.0]","[1.0,1.0]",popular(customer_0),, 22 | 4,8,customer_4,cool_car,"[0.0,1.0]","[1.0,1.0]",cool_car_rule,"[('customer_4', 'Car_4')]",['Car_4'] 23 | 4,8,customer_6,cool_car,"[0.0,1.0]","[1.0,1.0]",cool_car_rule,"[('customer_6', 'Car_4')]",['Car_4'] 24 | 4,8,customer_3,cool_pet,"[0.0,1.0]","[1.0,1.0]",cool_pet_rule,"[('customer_3', 'Pet_2')]",['Pet_2'] 25 | 4,8,customer_4,cool_pet,"[0.0,1.0]","[1.0,1.0]",cool_pet_rule,"[('customer_4', 'Pet_2')]",['Pet_2'] 26 | 4,9,customer_4,trendy,"[0.0,1.0]","[1.0,1.0]",trendy_rule,['customer_4'],['customer_4'] 27 | 5,10,popular-fac,popular-fac,"[0.0,1.0]","[1.0,1.0]",popular(customer_0),, 28 | 5,10,customer_4,cool_car,"[0.0,1.0]","[1.0,1.0]",cool_car_rule,"[('customer_4', 'Car_4')]",['Car_4'] 29 | 5,10,customer_6,cool_car,"[0.0,1.0]","[1.0,1.0]",cool_car_rule,"[('customer_6', 'Car_4')]",['Car_4'] 30 | 5,10,customer_3,cool_pet,"[0.0,1.0]","[1.0,1.0]",cool_pet_rule,"[('customer_3', 'Pet_2')]",['Pet_2'] 31 | 5,10,customer_4,cool_pet,"[0.0,1.0]","[1.0,1.0]",cool_pet_rule,"[('customer_4', 'Pet_2')]",['Pet_2'] 32 | 5,11,customer_4,trendy,"[0.0,1.0]","[1.0,1.0]",trendy_rule,['customer_4'],['customer_4'] 33 | -------------------------------------------------------------------------------- /examples/csv outputs/basic_rule_trace_nodes_20241119-012005.csv: -------------------------------------------------------------------------------- 1 | Time,Fixed-Point-Operation,Node,Label,Old Bound,New Bound,Occurred Due To 2 | 0,0,Mary,popular,-,"[1.0,1.0]",- 3 | 1,1,Mary,popular,-,"[1.0,1.0]",- 4 | 1,1,Justin,popular,-,"[1.0,1.0]",- 5 | 2,2,Mary,popular,-,"[1.0,1.0]",- 6 | 2,2,John,popular,-,"[1.0,1.0]",- 7 | 2,2,Justin,popular,-,"[1.0,1.0]",- 8 | -------------------------------------------------------------------------------- /examples/csv outputs/basic_rule_trace_nodes_20241125-114246.csv: -------------------------------------------------------------------------------- 1 | Time,Fixed-Point-Operation,Node,Label,Old Bound,New Bound,Occurred Due To 2 | 0,0,Mary,popular,-,"[1.0,1.0]",- 3 | 1,1,Mary,popular,-,"[1.0,1.0]",- 4 | 1,1,Justin,popular,-,"[1.0,1.0]",- 5 | 2,2,Mary,popular,-,"[1.0,1.0]",- 6 | 2,2,John,popular,-,"[1.0,1.0]",- 7 | 2,2,Justin,popular,-,"[1.0,1.0]",- 8 | -------------------------------------------------------------------------------- /examples/csv outputs/infer_edges_rule_trace_edges_20241119-140955.csv: -------------------------------------------------------------------------------- 1 | Time,Fixed-Point-Operation,Edge,Label,Old Bound,New Bound,Occurred Due To,Clause-1,Clause-2,Clause-3 2 | 0,0,"('Amsterdam_Airport_Schiphol', 'Yali')",isConnectedTo,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact,,, 3 | 0,0,"('Riga_International_Airport', 'Amsterdam_Airport_Schiphol')",isConnectedTo,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact,,, 4 | 0,0,"('Riga_International_Airport', 'Düsseldorf_Airport')",isConnectedTo,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact,,, 5 | 0,0,"('Chișinău_International_Airport', 'Riga_International_Airport')",isConnectedTo,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact,,, 6 | 0,0,"('Düsseldorf_Airport', 'Dubrovnik_Airport')",isConnectedTo,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact,,, 7 | 0,0,"('Pobedilovo_Airport', 'Vnukovo_International_Airport')",isConnectedTo,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact,,, 8 | 0,0,"('Dubrovnik_Airport', 'Athens_International_Airport')",isConnectedTo,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact,,, 9 | 0,0,"('Vnukovo_International_Airport', 'Hévíz-Balaton_Airport')",isConnectedTo,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact,,, 10 | 1,1,"('Vnukovo_International_Airport', 'Riga_International_Airport')",isConnectedTo,"[0.0,1.0]","[1.0,1.0]",connected_rule_1,"[('Riga_International_Airport', 'Amsterdam_Airport_Schiphol')]",['Amsterdam_Airport_Schiphol'],['Vnukovo_International_Airport'] 11 | -------------------------------------------------------------------------------- /examples/csv outputs/infer_edges_rule_trace_nodes_20241119-140955.csv: -------------------------------------------------------------------------------- 1 | Time,Fixed-Point-Operation,Node,Label,Old Bound,New Bound,Occurred Due To 2 | 0,0,Amsterdam_Airport_Schiphol,Amsterdam_Airport_Schiphol,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact 3 | 0,0,Riga_International_Airport,Riga_International_Airport,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact 4 | 0,0,Chișinău_International_Airport,Chișinău_International_Airport,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact 5 | 0,0,Yali,Yali,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact 6 | 0,0,Düsseldorf_Airport,Düsseldorf_Airport,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact 7 | 0,0,Pobedilovo_Airport,Pobedilovo_Airport,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact 8 | 0,0,Dubrovnik_Airport,Dubrovnik_Airport,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact 9 | 0,0,Hévíz-Balaton_Airport,Hévíz-Balaton_Airport,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact 10 | 0,0,Athens_International_Airport,Athens_International_Airport,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact 11 | 0,0,Vnukovo_International_Airport,Vnukovo_International_Airport,"[0.0,1.0]","[1.0,1.0]",graph-attribute-fact 12 | -------------------------------------------------------------------------------- /examples/custom_threshold_ex.py: -------------------------------------------------------------------------------- 1 | # Test if the simple program works with thresholds defined 2 | import pyreason as pr 3 | from pyreason import Threshold 4 | import networkx as nx 5 | 6 | # Reset PyReason 7 | pr.reset() 8 | pr.reset_rules() 9 | 10 | 11 | # Create an empty graph 12 | G = nx.DiGraph() 13 | 14 | # Add nodes 15 | nodes = ["TextMessage", "Zach", "Justin", "Michelle", "Amy"] 16 | G.add_nodes_from(nodes) 17 | 18 | # Add edges with attribute 'HaveAccess' 19 | G.add_edge("Zach", "TextMessage", HaveAccess=1) 20 | G.add_edge("Justin", "TextMessage", HaveAccess=1) 21 | G.add_edge("Michelle", "TextMessage", HaveAccess=1) 22 | G.add_edge("Amy", "TextMessage", HaveAccess=1) 23 | 24 | 25 | 26 | # Modify pyreason settings to make verbose 27 | pr.reset_settings() 28 | pr.settings.verbose = True # Print info to screen 29 | 30 | #load the graph 31 | pr.load_graph(G) 32 | 33 | # add custom thresholds 34 | user_defined_thresholds = [ 35 | Threshold("greater_equal", ("number", "total"), 1), 36 | Threshold("greater_equal", ("percent", "total"), 100), 37 | 38 | ] 39 | 40 | pr.add_rule( 41 | pr.Rule( 42 | "ViewedByAll(y) <- HaveAccess(x,y), Viewed(x)", 43 | "viewed_by_all_rule", 44 | custom_thresholds=user_defined_thresholds, 45 | ) 46 | ) 47 | 48 | pr.add_fact(pr.Fact("Viewed(Zach)", "seen-fact-zach", 0, 3)) 49 | pr.add_fact(pr.Fact("Viewed(Justin)", "seen-fact-justin", 0, 3)) 50 | pr.add_fact(pr.Fact("Viewed(Michelle)", "seen-fact-michelle", 1, 3)) 51 | pr.add_fact(pr.Fact("Viewed(Amy)", "seen-fact-amy", 2, 3)) 52 | 53 | # Run the program for three timesteps to see the diffusion take place 54 | interpretation = pr.reason(timesteps=3) 55 | 56 | # Display the changes in the interpretation for each timestep 57 | dataframes = pr.filter_and_sort_nodes(interpretation, ["ViewedByAll"]) 58 | for t, df in enumerate(dataframes): 59 | print(f"TIMESTEP - {t}") 60 | print(df) 61 | print() 62 | 63 | assert ( 64 | len(dataframes[0]) == 0 65 | ), "At t=0 the TextMessage should not have been ViewedByAll" 66 | assert ( 67 | len(dataframes[2]) == 1 68 | ), "At t=2 the TextMessage should have been ViewedByAll" 69 | 70 | # TextMessage should be ViewedByAll in t=2 71 | assert "TextMessage" in dataframes[2]["component"].values and dataframes[2].iloc[ 72 | 0 73 | ].ViewedByAll == [ 74 | 1, 75 | 1, 76 | ], "TextMessage should have ViewedByAll bounds [1,1] for t=2 timesteps" 77 | -------------------------------------------------------------------------------- /examples/infer_edges_ex.py: -------------------------------------------------------------------------------- 1 | import pyreason as pr 2 | import networkx as nx 3 | import matplotlib.pyplot as plt 4 | 5 | # Create a directed graph 6 | G = nx.DiGraph() 7 | 8 | # Add nodes with attributes 9 | nodes = [ 10 | ("Amsterdam_Airport_Schiphol", {"Amsterdam_Airport_Schiphol": 1}), 11 | ("Riga_International_Airport", {"Riga_International_Airport": 1}), 12 | ("Chișinău_International_Airport", {"Chișinău_International_Airport": 1}), 13 | ("Yali", {"Yali": 1}), 14 | ("Düsseldorf_Airport", {"Düsseldorf_Airport": 1}), 15 | ("Pobedilovo_Airport", {"Pobedilovo_Airport": 1}), 16 | ("Dubrovnik_Airport", {"Dubrovnik_Airport": 1}), 17 | ("Hévíz-Balaton_Airport", {"Hévíz-Balaton_Airport": 1}), 18 | ("Athens_International_Airport", {"Athens_International_Airport": 1}), 19 | ("Vnukovo_International_Airport", {"Vnukovo_International_Airport": 1}) 20 | ] 21 | 22 | G.add_nodes_from(nodes) 23 | 24 | # Add edges with 'isConnectedTo' attribute 25 | edges = [ 26 | ("Pobedilovo_Airport", "Vnukovo_International_Airport", {"isConnectedTo": 1}), 27 | ("Vnukovo_International_Airport", "Hévíz-Balaton_Airport", {"isConnectedTo": 1}), 28 | ("Düsseldorf_Airport", "Dubrovnik_Airport", {"isConnectedTo": 1}), 29 | ("Dubrovnik_Airport", "Athens_International_Airport", {"isConnectedTo": 1}), 30 | ("Riga_International_Airport", "Amsterdam_Airport_Schiphol", {"isConnectedTo": 1}), 31 | ("Riga_International_Airport", "Düsseldorf_Airport", {"isConnectedTo": 1}), 32 | ("Chișinău_International_Airport", "Riga_International_Airport", {"isConnectedTo": 1}), 33 | ("Amsterdam_Airport_Schiphol", "Yali", {"isConnectedTo": 1}), 34 | ] 35 | 36 | G.add_edges_from(edges) 37 | 38 | 39 | 40 | pr.reset() 41 | pr.reset_rules() 42 | # Modify pyreason settings to make verbose and to save the rule trace to a file 43 | pr.settings.verbose = True 44 | pr.settings.atom_trace = True 45 | pr.settings.memory_profile = False 46 | pr.settings.canonical = True 47 | pr.settings.inconsistency_check = False 48 | pr.settings.static_graph_facts = False 49 | pr.settings.output_to_file = False 50 | pr.settings.store_interpretation_changes = True 51 | pr.settings.save_graph_attributes_to_trace = True 52 | # Load all the files into pyreason 53 | pr.load_graph(G) 54 | pr.add_rule(pr.Rule('isConnectedTo(A, Y) <-1 isConnectedTo(Y, B), Amsterdam_Airport_Schiphol(B), Vnukovo_International_Airport(A)', 'connected_rule_1', infer_edges=True)) 55 | 56 | # Run the program for two timesteps to see the diffusion take place 57 | interpretation = pr.reason(timesteps=1) 58 | #pr.save_rule_trace(interpretation) 59 | 60 | # Display the changes in the interpretation for each timestep 61 | dataframes = pr.filter_and_sort_edges(interpretation, ['isConnectedTo']) 62 | for t, df in enumerate(dataframes): 63 | print(f'TIMESTEP - {t}') 64 | print(df) 65 | print() 66 | assert len(dataframes) == 2, 'Pyreason should run exactly 2 fixpoint operations' 67 | assert len(dataframes[1]) == 1, 'At t=1 there should be only 1 new isConnectedTo atom' 68 | assert ('Vnukovo_International_Airport', 'Riga_International_Airport') in dataframes[1]['component'].values.tolist() and dataframes[1]['isConnectedTo'].iloc[0] == [1, 1], '(Vnukovo_International_Airport, Riga_International_Airport) should have isConnectedTo bounds [1,1] for t=1 timesteps' 69 | -------------------------------------------------------------------------------- /examples/temporal_classifier_integration_ex.py: -------------------------------------------------------------------------------- 1 | import pyreason as pr 2 | import torch 3 | import torch.nn as nn 4 | import numpy as np 5 | import random 6 | 7 | # Set a seed for reproducibility. 8 | seed_value = 65 # Good Gap Gap 9 | # seed_value = 47 # Good Gap Good 10 | # seed_value = 43 # Good Good Good 11 | random.seed(seed_value) 12 | np.random.seed(seed_value) 13 | torch.manual_seed(seed_value) 14 | 15 | # --- Part 1: Weld Quality Model Integration --- 16 | 17 | # Create a dummy PyTorch model for detecting weld quality. 18 | # Each weld is represented by 3 features and is classified as "good" or "gap". 19 | weld_model = nn.Linear(3, 2) 20 | class_names = ["good", "gap"] 21 | 22 | # Define integration options: 23 | # Only consider probabilities above 0.5, adjust lower bound for high confidence, and use a snap value. 24 | interface_options = pr.ModelInterfaceOptions( 25 | threshold=0.5, 26 | set_lower_bound=True, 27 | set_upper_bound=False, 28 | snap_value=1.0 29 | ) 30 | 31 | # Wrap the model using LogicIntegratedClassifier. 32 | weld_quality_checker = pr.LogicIntegratedClassifier( 33 | weld_model, 34 | class_names, 35 | identifier="weld_object", 36 | interface_options=interface_options 37 | ) 38 | 39 | # --- Part 2: Simulate Weld Inspections Over Time --- 40 | pr.add_rule(pr.Rule("repair_attempted(weld_object) <-1 gap(weld_object)", "repair attempted rule")) 41 | pr.add_rule(pr.Rule("defective(weld_object) <-0 gap(weld_object), repair_attempted(weld_object)", "defective rule")) 42 | 43 | # Time step 1: Initial inspection shows the weld is good. 44 | features_t0 = torch.rand(1, 3) # Values chosen to indicate a good weld. 45 | logits_t0, probs_t0, classifier_facts_t0 = weld_quality_checker(features_t0, t1=0, t2=0) 46 | print("=== Weld Inspection at Time 0 ===") 47 | print("Logits:", logits_t0) 48 | print("Probabilities:", probs_t0) 49 | for fact in classifier_facts_t0: 50 | pr.add_fact(fact) 51 | 52 | # Time step 2: Second inspection detects a gap. 53 | features_t1 = torch.rand(1, 3) # Values chosen to simulate a gap. 54 | logits_t1, probs_t1, classifier_facts_t1 = weld_quality_checker(features_t1, t1=1, t2=1) 55 | print("\n=== Weld Inspection at Time 1 ===") 56 | print("Logits:", logits_t1) 57 | print("Probabilities:", probs_t1) 58 | for fact in classifier_facts_t1: 59 | pr.add_fact(fact) 60 | 61 | 62 | # Time step 3: Third inspection, the gap still persists. 63 | features_t2 = torch.rand(1, 3) # Values chosen to simulate persistent gap. 64 | logits_t2, probs_t2, classifier_facts_t2 = weld_quality_checker(features_t2, t1=2, t2=2) 65 | print("\n=== Weld Inspection at Time 2 ===") 66 | print("Logits:", logits_t2) 67 | print("Probabilities:", probs_t2) 68 | for fact in classifier_facts_t2: 69 | pr.add_fact(fact) 70 | 71 | 72 | # --- Part 3: Run the Reasoning Engine --- 73 | 74 | # Enable atom tracing for debugging the rule application process. 75 | pr.settings.atom_trace = True 76 | interpretation = pr.reason(timesteps=2) 77 | trace = pr.get_rule_trace(interpretation) 78 | 79 | print("\n=== Reasoning Rule Trace ===") 80 | print(trace[0]) -------------------------------------------------------------------------------- /initialize.py: -------------------------------------------------------------------------------- 1 | # Run this script after cloning repository to generate the numba caches. This script runs the hello-world program internally 2 | print('Initializing PyReason caches') 3 | import pyreason as pr 4 | -------------------------------------------------------------------------------- /jobs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore -------------------------------------------------------------------------------- /lit/README.md: -------------------------------------------------------------------------------- 1 | # PyReason Literature Page 2 | 3 | Extended paper with supplemental and appendix sections: https://arxiv.org/abs/2302.13482 4 | -------------------------------------------------------------------------------- /media/group_chat_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/media/group_chat_graph.png -------------------------------------------------------------------------------- /media/hello_world_friends_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/media/hello_world_friends_graph.png -------------------------------------------------------------------------------- /media/infer_edges1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/media/infer_edges1.png -------------------------------------------------------------------------------- /media/infer_edges11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/media/infer_edges11.png -------------------------------------------------------------------------------- /media/infer_edges2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/media/infer_edges2.png -------------------------------------------------------------------------------- /media/pyreason_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/media/pyreason_logo.jpg -------------------------------------------------------------------------------- /output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore -------------------------------------------------------------------------------- /profiling/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['setuptools>=42'] 3 | build-backend = 'setuptools.build_meta' 4 | 5 | [tool.pytest.ini_options] 6 | pythonpath = [ 7 | "." 8 | ] 9 | -------------------------------------------------------------------------------- /pyreason/.cache_status.yaml: -------------------------------------------------------------------------------- 1 | initialized: false 2 | -------------------------------------------------------------------------------- /pyreason/__init__.py: -------------------------------------------------------------------------------- 1 | # Set numba environment variable 2 | import os 3 | package_path = os.path.abspath(os.path.dirname(__file__)) 4 | cache_path = os.path.join(package_path, 'cache') 5 | cache_status_path = os.path.join(package_path, '.cache_status.yaml') 6 | os.environ['NUMBA_CACHE_DIR'] = cache_path 7 | 8 | 9 | from pyreason.pyreason import * 10 | import yaml 11 | from importlib.metadata import version 12 | from pkg_resources import get_distribution, DistributionNotFound 13 | 14 | try: 15 | __version__ = get_distribution(__name__).version 16 | except DistributionNotFound: 17 | # package is not installed 18 | pass 19 | 20 | 21 | with open(cache_status_path) as file: 22 | cache_status = yaml.safe_load(file) 23 | 24 | if not cache_status['initialized']: 25 | print('Imported PyReason for the first time. Initializing caches for faster runtimes ... this will take a minute') 26 | graph_path = os.path.join(package_path, 'examples', 'hello-world', 'friends_graph.graphml') 27 | 28 | settings.verbose = False 29 | load_graphml(graph_path) 30 | add_rule(Rule('popular(x) <-1 popular(y), Friends(x,y), owns(y,z), owns(x,z)', 'popular_rule')) 31 | add_fact(Fact('popular(Mary)', 'popular_fact', 0, 2)) 32 | reason(timesteps=2) 33 | 34 | reset() 35 | reset_rules() 36 | print('PyReason initialized!') 37 | print() 38 | 39 | # Update cache status 40 | cache_status['initialized'] = True 41 | with open(cache_status_path, 'w') as file: 42 | yaml.dump(cache_status, file) 43 | -------------------------------------------------------------------------------- /pyreason/examples/hello-world/friends_graph.graphml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 1 13 | 14 | 15 | 1 16 | 17 | 18 | 1 19 | 20 | 21 | 1 22 | 23 | 24 | 1 25 | 26 | 27 | 1 28 | 29 | 30 | 1 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /pyreason/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/annotation_functions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/annotation_functions/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/annotation_functions/annotation_functions.py: -------------------------------------------------------------------------------- 1 | # List of annotation functions will come here. All functions to be numba decorated and compatible 2 | # Each function has access to the interpretations at a particular timestep, and the qualified nodes and qualified edges that made the rule fire 3 | import numba 4 | import numpy as np 5 | 6 | import pyreason.scripts.numba_wrapper.numba_types.interval_type as interval 7 | 8 | @numba.njit 9 | def _get_weighted_sum(annotations, weights, mode='lower'): 10 | """ 11 | Returns weighted sum plus the total number of annotations 12 | """ 13 | # List containing the weighted sum for lower bound for each clause 14 | weighted_sum = np.arange(0, dtype=np.float64) 15 | annotation_cnt = 0 16 | for i, clause in enumerate(annotations): 17 | s = 0 18 | for annotation in clause: 19 | annotation_cnt += 1 20 | if mode=='lower': 21 | s += annotation.lower * weights[i] 22 | elif mode=='upper': 23 | s += annotation.upper * weights[i] 24 | weighted_sum = np.append(weighted_sum, s) 25 | 26 | return weighted_sum, annotation_cnt 27 | 28 | @numba.njit 29 | def _check_bound(lower, upper): 30 | if lower > upper: 31 | return (0, 1) 32 | else: 33 | l = min(lower, 1) 34 | u = min(upper, 1) 35 | return (l, u) 36 | 37 | 38 | @numba.njit 39 | def average(annotations, weights): 40 | """ 41 | Take average of lower bounds to make new lower bound, take average of upper bounds to make new upper bound 42 | """ 43 | weighted_sum_lower, n = _get_weighted_sum(annotations, weights, mode='lower') 44 | weighted_sum_upper, n = _get_weighted_sum(annotations, weights, mode='upper') 45 | 46 | # n cannot be zero otherwise rule would not have fired 47 | avg_lower = np.sum(weighted_sum_lower) / n 48 | avg_upper = np.sum(weighted_sum_upper) / n 49 | 50 | lower, upper = _check_bound(avg_lower, avg_upper) 51 | 52 | return interval.closed(lower, upper) 53 | 54 | @numba.njit 55 | def average_lower(annotations, weights): 56 | """ 57 | Take average of lower bounds to make new lower bound, take max of upper bounds to make new upper bound 58 | """ 59 | weighted_sum_lower, n = _get_weighted_sum(annotations, weights, mode='lower') 60 | 61 | avg_lower = np.sum(weighted_sum_lower) / n 62 | 63 | max_upper = 0 64 | for clause in annotations: 65 | for annotation in clause: 66 | max_upper = annotation.upper if annotation.upper > max_upper else max_upper 67 | 68 | lower, upper = _check_bound(avg_lower, max_upper) 69 | 70 | return interval.closed(lower, upper) 71 | 72 | @numba.njit 73 | def maximum(annotations, weights): 74 | """ 75 | Take max of lower bounds to make new lower bound, take max of upper bounds to make new upper bound 76 | """ 77 | weighted_sum_lower, n = _get_weighted_sum(annotations, weights, mode='lower') 78 | weighted_sum_upper, n = _get_weighted_sum(annotations, weights, mode='upper') 79 | 80 | max_lower = np.max(weighted_sum_lower) 81 | max_upper = np.max(weighted_sum_upper) 82 | 83 | lower, upper = _check_bound(max_lower, max_upper) 84 | 85 | return interval.closed(lower, upper) 86 | 87 | 88 | @numba.njit 89 | def minimum(annotations, weights): 90 | """ 91 | Take min of lower bounds to make new lower bound, take min of upper bounds to make new upper bound 92 | """ 93 | weighted_sum_lower, n = _get_weighted_sum(annotations, weights, mode='lower') 94 | weighted_sum_upper, n = _get_weighted_sum(annotations, weights, mode='upper') 95 | 96 | min_lower = np.min(weighted_sum_lower) 97 | min_upper = np.min(weighted_sum_upper) 98 | 99 | lower, upper = _check_bound(min_lower, min_upper) 100 | 101 | return interval.closed(lower, upper) 102 | 103 | 104 | -------------------------------------------------------------------------------- /pyreason/scripts/components/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/components/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/components/label.py: -------------------------------------------------------------------------------- 1 | class Label: 2 | 3 | def __init__(self, value): 4 | self._value = value 5 | 6 | def get_value(self): 7 | return self._value 8 | 9 | def __eq__(self, label): 10 | result = (self._value == label.get_value()) and isinstance(label, type(self)) 11 | return result 12 | 13 | def __str__(self): 14 | return self._value 15 | 16 | def __hash__(self): 17 | return hash(str(self)) 18 | 19 | def __repr__(self): 20 | return self.get_value() -------------------------------------------------------------------------------- /pyreason/scripts/components/world.py: -------------------------------------------------------------------------------- 1 | import numba 2 | import pyreason.scripts.numba_wrapper.numba_types.interval_type as interval 3 | import pyreason.scripts.numba_wrapper.numba_types.label_type as label 4 | 5 | 6 | class World: 7 | 8 | def __init__(self, labels): 9 | self._labels = labels 10 | self._world = numba.typed.Dict.empty(key_type=label.label_type, value_type=interval.interval_type) 11 | for l in labels: 12 | self._world[l] = interval.closed(0.0, 1.0) 13 | 14 | @property 15 | def world(self): 16 | return self._world 17 | 18 | def make_world(labels, world): 19 | w = World(labels) 20 | w._world = world 21 | return w 22 | 23 | def is_satisfied(self, label, interval): 24 | result = False 25 | 26 | bnd = self._world[label] 27 | result = bnd in interval 28 | 29 | return result 30 | 31 | def update(self, label, interval): 32 | lwanted = None 33 | bwanted = None 34 | 35 | current_bnd = self._world[label] 36 | new_bnd = current_bnd.intersection(interval) 37 | self._world[label] = new_bnd 38 | 39 | def get_bound(self, label): 40 | result = None 41 | 42 | result = self._world[label] 43 | return result 44 | 45 | def get_world(self): 46 | return self._world 47 | 48 | 49 | def __str__(self): 50 | result = '' 51 | for label in self._world.keys(): 52 | result = result + label.get_value() + ',' + self._world[label].to_str() + '\n' 53 | 54 | return result -------------------------------------------------------------------------------- /pyreason/scripts/facts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/facts/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/facts/fact.py: -------------------------------------------------------------------------------- 1 | import pyreason.scripts.utils.fact_parser as fact_parser 2 | import pyreason.scripts.numba_wrapper.numba_types.label_type as label 3 | 4 | 5 | class Fact: 6 | def __init__(self, fact_text: str, name: str = None, start_time: int = 0, end_time: int = 0, static: bool = False): 7 | """Define a PyReason fact that can be loaded into the program using `pr.add_fact()` 8 | 9 | :param fact_text: The fact in text format. Example: `'pred(x,y) : [0.2, 1]'` or `'pred(x,y) : True'` 10 | :type fact_text: str 11 | :param name: The name of the fact. This will appear in the trace so that you know when it was applied 12 | :type name: str 13 | :param start_time: The timestep at which this fact becomes active 14 | :type start_time: int 15 | :param end_time: The last timestep this fact is active 16 | :type end_time: int 17 | :param static: If the fact should be active for the entire program. In which case `start_time` and `end_time` will be ignored 18 | :type static: bool 19 | """ 20 | pred, component, bound, fact_type = fact_parser.parse_fact(fact_text) 21 | self.name = name 22 | self.start_time = start_time 23 | self.end_time = end_time 24 | self.static = static 25 | self.pred = label.Label(pred) 26 | self.component = component 27 | self.bound = bound 28 | self.type = fact_type 29 | 30 | def __str__(self): 31 | s = f'{self.pred}({self.component}) : {self.bound}' 32 | if self.static: 33 | s += ' | static' 34 | else: 35 | s += f' | start: {self.start_time} -> end: {self.end_time}' 36 | return s 37 | -------------------------------------------------------------------------------- /pyreason/scripts/facts/fact_edge.py: -------------------------------------------------------------------------------- 1 | class Fact: 2 | 3 | def __init__(self, name, component, label, interval, t_lower, t_upper, static=False): 4 | self._name = name 5 | self._t_upper = t_upper 6 | self._t_lower = t_lower 7 | self._component = component 8 | self._label = label 9 | self._interval = interval 10 | self._static = static 11 | 12 | def get_name(self): 13 | return self._name 14 | 15 | def set_name(self, name): 16 | self._name = name 17 | 18 | def get_component(self): 19 | return self._component 20 | 21 | def get_label(self): 22 | return self._label 23 | 24 | def get_bound(self): 25 | return self._interval 26 | 27 | def get_time_lower(self): 28 | return self._t_lower 29 | 30 | def get_time_upper(self): 31 | return self._t_upper 32 | 33 | def __str__(self): 34 | fact = { 35 | "type": 'pyreason edge fact', 36 | "name": self._name, 37 | "component": self._component, 38 | "label": self._label, 39 | "confidence": self._interval, 40 | "time": '[' + str(self._t_lower) + ',' + str(self._t_upper) + ']' 41 | } 42 | return fact 43 | -------------------------------------------------------------------------------- /pyreason/scripts/facts/fact_node.py: -------------------------------------------------------------------------------- 1 | class Fact: 2 | 3 | def __init__(self, name, component, label, interval, t_lower, t_upper, static=False): 4 | self._name = name 5 | self._t_upper = t_upper 6 | self._t_lower = t_lower 7 | self._component = component 8 | self._label = label 9 | self._interval = interval 10 | self._static = static 11 | 12 | def get_name(self): 13 | return self._name 14 | 15 | def set_name(self, name): 16 | self._name = name 17 | 18 | def get_component(self): 19 | return self._component 20 | 21 | def get_label(self): 22 | return self._label 23 | 24 | def get_bound(self): 25 | return self._interval 26 | 27 | def get_time_lower(self): 28 | return self._t_lower 29 | 30 | def get_time_upper(self): 31 | return self._t_upper 32 | 33 | def __str__(self): 34 | fact = { 35 | "type": 'pyreason node fact', 36 | "name": self._name, 37 | "component": self._component, 38 | "label": self._label, 39 | "confidence": self._interval, 40 | "time": '[' + str(self._t_lower) + ',' + str(self._t_upper) + ']' 41 | } 42 | return fact 43 | -------------------------------------------------------------------------------- /pyreason/scripts/interpretation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/interpretation/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/interpretation/interpretation_dict.py: -------------------------------------------------------------------------------- 1 | class InterpretationDict(dict): 2 | """ 3 | This class is specific for the interpretation for a specific timestep. 4 | """ 5 | def __int__(self): 6 | super().__init__() 7 | 8 | def __setitem__(self, key, value): 9 | assert len(value) == 2, 'Lower bound and Upper bound are required to set an Interpretation' 10 | self.__dict__[key] = (value[0], value[1]) 11 | 12 | def __getitem__(self, key): 13 | if key not in self.__dict__.keys(): 14 | return tuple((0, 1)) 15 | else: 16 | return self.__dict__[key] 17 | 18 | def __repr__(self): 19 | return repr(self.__dict__) 20 | 21 | def __len__(self): 22 | return len(self.__dict__) 23 | 24 | def __delitem__(self, key): 25 | del self.__dict__[key] 26 | 27 | def clear(self): 28 | return self.__dict__.clear() 29 | 30 | def copy(self): 31 | return self.__dict__.copy() 32 | 33 | def has_key(self, k): 34 | return k in self.__dict__ 35 | 36 | def update(self, *args, **kwargs): 37 | return self.__dict__.update(*args, **kwargs) 38 | 39 | def keys(self): 40 | return self.__dict__.keys() 41 | 42 | def values(self): 43 | return self.__dict__.values() 44 | 45 | def items(self): 46 | return self.__dict__.items() 47 | 48 | def pop(self, *args): 49 | return self.__dict__.pop(*args) 50 | 51 | def __cmp__(self, dict_): 52 | return self.__cmp__(self.__dict__, dict_) 53 | 54 | def __contains__(self, item): 55 | return item in self.__dict__ 56 | 57 | def __iter__(self): 58 | return iter(self.__dict__) 59 | -------------------------------------------------------------------------------- /pyreason/scripts/interval/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/interval/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/interval/interval.py: -------------------------------------------------------------------------------- 1 | from numba.experimental import structref 2 | from numba import njit 3 | import numpy as np 4 | 5 | 6 | class Interval(structref.StructRefProxy): 7 | def __new__(cls, l, u, s=False): 8 | return structref.StructRefProxy.__new__(cls, l, u, s, l, u) 9 | 10 | @property 11 | @njit 12 | def lower(self): 13 | return self.l 14 | 15 | @property 16 | @njit 17 | def upper(self): 18 | return self.u 19 | 20 | @property 21 | @njit 22 | def static(self): 23 | return self.s 24 | 25 | @property 26 | @njit 27 | def prev_lower(self): 28 | return self.prev_l 29 | 30 | @property 31 | @njit 32 | def prev_upper(self): 33 | return self.prev_u 34 | 35 | @njit 36 | def set_lower_upper(self, l, u): 37 | self.l = l 38 | self.u = u 39 | 40 | @njit 41 | def reset(self): 42 | self.prev_l = self.l 43 | self.prev_u = self.u 44 | self.l = 0 45 | self.u = 1 46 | 47 | @njit 48 | def set_static(self, static): 49 | self.s = static 50 | 51 | @njit 52 | def is_static(self): 53 | return self.s 54 | 55 | @njit 56 | def has_changed(self): 57 | if self.lower==self.prev_lower and self.upper==self.prev_upper: 58 | return False 59 | else: 60 | return True 61 | 62 | @njit 63 | def intersection(self, interval): 64 | lower = max(self.lower, interval.lower) 65 | upper = min(self.upper, interval.upper) 66 | if lower > upper: 67 | lower = np.float32(0) 68 | upper = np.float32(1) 69 | return Interval(lower, upper, False, self.lower, self.upper) 70 | 71 | def to_str(self): 72 | return self.__repr__() 73 | 74 | def __eq__(self, interval): 75 | if interval.lower==self.lower and interval.upper==self.upper: 76 | return True 77 | else: 78 | return False 79 | 80 | def __repr__(self): 81 | return f'[{self.lower},{self.upper}]' 82 | 83 | def __hash__(self): 84 | return hash((self.lower, self.upper)) 85 | 86 | def __contains__(self, item): 87 | if self.lower <= item.lower and self.upper >= item.upper: 88 | return True 89 | else: 90 | return False 91 | -------------------------------------------------------------------------------- /pyreason/scripts/learning/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/learning/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/learning/classification/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/learning/classification/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/learning/classification/classifier.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | import torch.nn 4 | import torch.nn.functional as F 5 | 6 | from pyreason.scripts.facts.fact import Fact 7 | from pyreason.scripts.learning.utils.model_interface import ModelInterfaceOptions 8 | 9 | 10 | class LogicIntegratedClassifier(torch.nn.Module): 11 | """ 12 | Class to integrate a PyTorch model with PyReason. The output of the model is returned to the 13 | user in the form of PyReason facts. The user can then add these facts to the logic program and reason using them. 14 | """ 15 | def __init__(self, model, class_names: List[str], identifier: str = 'classifier', interface_options: ModelInterfaceOptions = None): 16 | """ 17 | :param model: PyTorch model to be integrated. 18 | :param class_names: List of class names for the model output. 19 | :param identifier: Identifier for the model, used as the constant in the facts. 20 | :param interface_options: Options for the model interface, including threshold and snapping behavior. 21 | """ 22 | super(LogicIntegratedClassifier, self).__init__() 23 | self.model = model 24 | self.class_names = class_names 25 | self.identifier = identifier 26 | self.interface_options = interface_options 27 | 28 | def get_class_facts(self, t1: int, t2: int) -> List[Fact]: 29 | """ 30 | Return PyReason facts to create nodes for each class. Each class node will have bounds `[1,1]` with the 31 | predicate corresponding to the model name. 32 | :param t1: Start time for the facts 33 | :param t2: End time for the facts 34 | :return: List of PyReason facts 35 | """ 36 | facts = [] 37 | for c in self.class_names: 38 | fact = Fact(f'{c}({self.identifier})', name=f'{self.identifier}-{c}-fact', start_time=t1, end_time=t2) 39 | facts.append(fact) 40 | return facts 41 | 42 | def forward(self, x, t1: int = 0, t2: int = 0) -> Tuple[torch.Tensor, torch.Tensor, List[Fact]]: 43 | """ 44 | Forward pass of the model 45 | :param x: Input tensor 46 | :param t1: Start time for the facts 47 | :param t2: End time for the facts 48 | :return: Output tensor 49 | """ 50 | output = self.model(x) 51 | 52 | # Convert logits to probabilities assuming a multi-class classification. 53 | probabilities = F.softmax(output, dim=1).squeeze() 54 | opts = self.interface_options 55 | 56 | # Prepare threshold tensor. 57 | threshold = torch.tensor(opts.threshold, dtype=probabilities.dtype, device=probabilities.device) 58 | condition = probabilities > threshold 59 | 60 | if opts.snap_value is not None: 61 | snap_value = torch.tensor(opts.snap_value, dtype=probabilities.dtype, device=probabilities.device) 62 | # For values that pass the threshold: 63 | lower_val = snap_value if opts.set_lower_bound else torch.tensor(0.0, dtype=probabilities.dtype, 64 | device=probabilities.device) 65 | upper_val = snap_value if opts.set_upper_bound else torch.tensor(1.0, dtype=probabilities.dtype, 66 | device=probabilities.device) 67 | else: 68 | # If no snap_value is provided, keep original probabilities for those passing threshold. 69 | lower_val = probabilities if opts.set_lower_bound else torch.zeros_like(probabilities) 70 | upper_val = probabilities if opts.set_upper_bound else torch.ones_like(probabilities) 71 | 72 | # For probabilities that pass the threshold, apply the above; else, bounds are fixed to [0,1]. 73 | lower_bounds = torch.where(condition, lower_val, torch.zeros_like(probabilities)) 74 | upper_bounds = torch.where(condition, upper_val, torch.ones_like(probabilities)) 75 | 76 | # Convert bounds to Python floats for fact creation. 77 | bounds_list = [] 78 | for i in range(len(self.class_names)): 79 | lower = lower_bounds[i].item() 80 | upper = upper_bounds[i].item() 81 | bounds_list.append([lower, upper]) 82 | 83 | # Define time bounds for the facts. 84 | facts = [] 85 | for class_name, bounds in zip(self.class_names, bounds_list): 86 | lower, upper = bounds 87 | fact_str = f'{class_name}({self.identifier}) : [{lower:.3f}, {upper:.3f}]' 88 | fact = Fact(fact_str, name=f'{self.identifier}-{class_name}-fact', start_time=t1, end_time=t2) 89 | facts.append(fact) 90 | return output, probabilities, facts 91 | 92 | -------------------------------------------------------------------------------- /pyreason/scripts/learning/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/learning/utils/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/learning/utils/model_interface.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | 5 | @dataclass 6 | class ModelInterfaceOptions: 7 | threshold: float = 0.5 8 | set_lower_bound: bool = True 9 | set_upper_bound: bool = True 10 | snap_value: Optional[float] = 1.0 11 | -------------------------------------------------------------------------------- /pyreason/scripts/numba_wrapper/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/numba_wrapper/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/numba_wrapper/numba_types/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/numba_wrapper/numba_types/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/numba_wrapper/numba_types/interval_type.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import numpy as np 3 | import numba 4 | from numba import njit 5 | from numba.core import types 6 | from numba.experimental import structref 7 | from numba.core.extending import overload_method, overload_attribute, overload 8 | 9 | from pyreason.scripts.interval.interval import Interval 10 | 11 | 12 | 13 | @structref.register 14 | class IntervalType(numba.types.StructRef): 15 | def preprocess_fields(self, fields): 16 | return tuple((name, types.unliteral(typ)) for name, typ in fields) 17 | 18 | interval_fields = [ 19 | ('l', types.float64), 20 | ('u', types.float64), 21 | ('s', types.boolean), 22 | ('prev_l', types.float64), 23 | ('prev_u', types.float64) 24 | ] 25 | 26 | interval_type = IntervalType(interval_fields) 27 | structref.define_proxy(Interval, IntervalType, ["l", "u", 's', 'prev_l', 'prev_u']) 28 | 29 | 30 | @overload_attribute(IntervalType, "lower") 31 | def get_lower(interval): 32 | def getter(interval): 33 | return interval.l 34 | return getter 35 | 36 | @overload_attribute(IntervalType, "upper") 37 | def get_upper(interval): 38 | def getter(interval): 39 | return interval.u 40 | return getter 41 | 42 | @overload_attribute(IntervalType, "prev_lower") 43 | def prev_lower(interval): 44 | def getter(interval): 45 | return interval.prev_l 46 | return getter 47 | 48 | @overload_attribute(IntervalType, "prev_upper") 49 | def prev_upper(interval): 50 | def getter(interval): 51 | return interval.prev_u 52 | return getter 53 | 54 | 55 | @overload_method(IntervalType, "intersection") 56 | def intersection(self, interval): 57 | def impl(self, interval): 58 | lower = max(self.lower, interval.lower) 59 | upper = min(self.upper, interval.upper) 60 | if lower > upper: 61 | lower = np.float32(0) 62 | upper = np.float32(1) 63 | return Interval(lower, upper, False, self.prev_lower, self.prev_upper) 64 | 65 | return impl 66 | 67 | @overload_method(IntervalType, 'set_lower_upper') 68 | def set_lower_upper(interval, l, u): 69 | def impl(interval, l, u): 70 | interval.l = np.float64(l) 71 | interval.u = np.float64(u) 72 | return impl 73 | 74 | @overload_method(IntervalType, 'reset') 75 | def reset(interval): 76 | def impl(interval): 77 | # Set previous bounds 78 | interval.prev_l = interval.l 79 | interval.prev_u = interval.u 80 | # Then set new bounds 81 | interval.l = np.float64(0) 82 | interval.u = np.float64(1) 83 | return impl 84 | 85 | @overload_method(IntervalType, 'set_static') 86 | def set_static(interval, s): 87 | def impl(interval, s): 88 | interval.s = s 89 | return impl 90 | 91 | @overload_method(IntervalType, 'is_static') 92 | def is_static(interval): 93 | def impl(interval): 94 | return interval.s 95 | return impl 96 | 97 | @overload_method(IntervalType, 'has_changed') 98 | def has_changed(interval): 99 | def impl(interval): 100 | if interval.lower==interval.prev_lower and interval.upper==interval.prev_upper: 101 | return False 102 | else: 103 | return True 104 | return impl 105 | 106 | @overload_method(IntervalType, 'copy') 107 | def copy(interval): 108 | def impl(interval): 109 | return Interval(interval.lower, interval.upper, interval.s, interval.prev_l, interval.prev_u) 110 | return impl 111 | 112 | 113 | @overload(operator.eq) 114 | def interval_eq(interval_1, interval_2): 115 | if isinstance(interval_1, IntervalType) and isinstance(interval_2, IntervalType): 116 | def impl(interval_1, interval_2): 117 | if interval_1.lower == interval_2.lower and interval_1.upper == interval_2.upper: 118 | return True 119 | else: 120 | return False 121 | return impl 122 | 123 | @overload(operator.ne) 124 | def interval_ne(interval_1, interval_2): 125 | if isinstance(interval_1, IntervalType) and isinstance(interval_2, IntervalType): 126 | def impl(interval_1, interval_2): 127 | if interval_1.lower != interval_2.lower or interval_1.upper != interval_2.upper: 128 | return True 129 | else: 130 | return False 131 | return impl 132 | 133 | @overload(operator.contains) 134 | def interval_contains(interval_1, interval_2): 135 | def impl(interval_1, interval_2): 136 | if interval_1.lower <= interval_2.lower and interval_1.upper >= interval_2.upper: 137 | return True 138 | else: 139 | return False 140 | return impl 141 | 142 | 143 | @njit 144 | def closed(lower, upper, static=False): 145 | return Interval(np.float64(lower), np.float64(upper), static, np.float64(lower), np.float64(upper)) 146 | -------------------------------------------------------------------------------- /pyreason/scripts/numba_wrapper/numba_types/label_type.py: -------------------------------------------------------------------------------- 1 | from pyreason.scripts.components.label import Label 2 | 3 | import operator 4 | from numba import types 5 | from numba.extending import typeof_impl 6 | from numba.extending import type_callable 7 | from numba.extending import models, register_model 8 | from numba.extending import make_attribute_wrapper 9 | from numba.extending import overload_method, overload 10 | from numba.extending import lower_builtin 11 | from numba.core import cgutils 12 | from numba.extending import unbox, NativeValue, box 13 | 14 | 15 | # Create new numba type 16 | class LabelType(types.Type): 17 | def __init__(self): 18 | super(LabelType, self).__init__(name='Label') 19 | 20 | label_type = LabelType() 21 | 22 | 23 | # Type inference 24 | @typeof_impl.register(Label) 25 | def typeof_label(val, c): 26 | return label_type 27 | 28 | 29 | # Construct object from Numba functions 30 | @type_callable(Label) 31 | def type_label(context): 32 | def typer(value): 33 | if isinstance(value, types.UnicodeType): 34 | return label_type 35 | return typer 36 | 37 | 38 | # Define native representation: datamodel 39 | @register_model(LabelType) 40 | class LabelModel(models.StructModel): 41 | def __init__(self, dmm, fe_type): 42 | members = [ 43 | ('value', types.string) 44 | ] 45 | models.StructModel.__init__(self, dmm, fe_type, members) 46 | 47 | 48 | # Expose datamodel attributes 49 | make_attribute_wrapper(LabelType, 'value', 'value') 50 | 51 | # Implement constructor 52 | @lower_builtin(Label, types.string) 53 | def impl_label(context, builder, sig, args): 54 | typ = sig.return_type 55 | value = args[0] 56 | label = cgutils.create_struct_proxy(typ)(context, builder) 57 | label.value = value 58 | return label._getvalue() 59 | 60 | # Expose properties 61 | @overload_method(LabelType, "get_value") 62 | def get_id(label): 63 | def getter(label): 64 | return label.value 65 | return getter 66 | 67 | @overload(operator.eq) 68 | def label_eq(label_1, label_2): 69 | if isinstance(label_1, LabelType) and isinstance(label_2, LabelType): 70 | def impl(label_1, label_2): 71 | if label_1.value == label_2.value: 72 | return True 73 | else: 74 | return False 75 | return impl 76 | 77 | @overload(hash) 78 | def label_hash(label): 79 | def impl(label): 80 | return hash(label.value) 81 | return impl 82 | 83 | 84 | 85 | # Tell numba how to make native 86 | @unbox(LabelType) 87 | def unbox_label(typ, obj, c): 88 | value_obj = c.pyapi.object_getattr_string(obj, "_value") 89 | label = cgutils.create_struct_proxy(typ)(c.context, c.builder) 90 | label.value = c.unbox(types.string, value_obj).value 91 | c.pyapi.decref(value_obj) 92 | is_error = cgutils.is_not_null(c.builder, c.pyapi.err_occurred()) 93 | return NativeValue(label._getvalue(), is_error=is_error) 94 | 95 | 96 | 97 | @box(LabelType) 98 | def box_label(typ, val, c): 99 | label = cgutils.create_struct_proxy(typ)(c.context, c.builder, value=val) 100 | class_obj = c.pyapi.unserialize(c.pyapi.serialize_object(Label)) 101 | value_obj = c.box(types.string, label.value) 102 | res = c.pyapi.call_function_objargs(class_obj, (value_obj,)) 103 | c.pyapi.decref(value_obj) 104 | c.pyapi.decref(class_obj) 105 | return res 106 | 107 | -------------------------------------------------------------------------------- /pyreason/scripts/numba_wrapper/numba_types/world_type.py: -------------------------------------------------------------------------------- 1 | import numba 2 | import pyreason.scripts.numba_wrapper.numba_types.interval_type as interval 3 | import pyreason.scripts.numba_wrapper.numba_types.label_type as label 4 | from pyreason.scripts.components.world import World 5 | 6 | import operator 7 | from numba import types 8 | from numba.extending import typeof_impl 9 | from numba.extending import type_callable 10 | from numba.extending import models, register_model 11 | from numba.extending import make_attribute_wrapper 12 | from numba.extending import overload_method, overload 13 | from numba.extending import lower_builtin 14 | from numba.core import cgutils 15 | from numba.extending import unbox, NativeValue, box 16 | from numba.core.typing import signature 17 | 18 | 19 | # Create new numba type 20 | class WorldType(types.Type): 21 | def __init__(self): 22 | super(WorldType, self).__init__(name='World') 23 | 24 | world_type = WorldType() 25 | 26 | 27 | # Type inference 28 | @typeof_impl.register(World) 29 | def typeof_world(val, c): 30 | return world_type 31 | 32 | 33 | # Construct object from Numba functions 34 | # Constructor for internal use only 35 | @type_callable(World) 36 | def type_world(context): 37 | def typer(labels, world): 38 | if isinstance(labels, types.ListType) and isinstance(world, types.DictType): 39 | return world_type 40 | return typer 41 | 42 | @type_callable(World) 43 | def type_world(context): 44 | def typer(labels): 45 | if isinstance(labels, types.ListType): 46 | return world_type 47 | return typer 48 | 49 | 50 | # Define native representation: datamodel 51 | @register_model(WorldType) 52 | class WorldModel(models.StructModel): 53 | def __init__(self, dmm, fe_type): 54 | members = [ 55 | ('labels', types.ListType(label.label_type)), 56 | ('world', types.DictType(label.label_type, interval.interval_type)) 57 | ] 58 | models.StructModel.__init__(self, dmm, fe_type, members) 59 | 60 | 61 | # Expose datamodel attributes 62 | make_attribute_wrapper(WorldType, 'labels', 'labels') 63 | make_attribute_wrapper(WorldType, 'world', 'world') 64 | 65 | # Implement constructor 66 | # Constructor for internal use only 67 | @lower_builtin(World, types.ListType(label.label_type), types.DictType(label.label_type, interval.interval_type)) 68 | def impl_world(context, builder, sig, args): 69 | # context.build_map(builder, ) 70 | typ = sig.return_type 71 | l, wo = args 72 | context.nrt.incref(builder, types.DictType(label.label_type, interval.interval_type), wo) 73 | context.nrt.incref(builder, types.ListType(label.label_type), l) 74 | w = cgutils.create_struct_proxy(typ)(context, builder) 75 | w.labels = l 76 | w.world = wo 77 | return w._getvalue() 78 | 79 | @lower_builtin(World, types.ListType(label.label_type)) 80 | def impl_world(context, builder, sig, args): 81 | def make_world(l): 82 | d = numba.typed.Dict.empty(key_type=label.label_type, value_type=interval.interval_type) 83 | for lab in l: 84 | d[lab] = interval.closed(0.0, 1.0) 85 | w = World(l, d) 86 | return w 87 | 88 | w = context.compile_internal(builder, make_world, sig, args) 89 | return w 90 | 91 | 92 | # Expose properties 93 | @overload_method(WorldType, 'is_satisfied') 94 | def is_satisfied(world, label, interval): 95 | def impl(world, label, interval): 96 | result = False 97 | bnd = world.world[label] 98 | result = bnd in interval 99 | 100 | return result 101 | return impl 102 | 103 | @overload_method(WorldType, 'update') 104 | def update(w, label, interval): 105 | def impl(w, label, interval): 106 | current_bnd = w.world[label] 107 | new_bnd = current_bnd.intersection(interval) 108 | w.world[label] = new_bnd 109 | return impl 110 | 111 | @overload_method(WorldType, 'get_bound') 112 | def get_bound(world, label): 113 | def impl(world, label): 114 | result = None 115 | result = world.world[label] 116 | return result 117 | return impl 118 | 119 | 120 | # Tell numba how to make native 121 | @unbox(WorldType) 122 | def unbox_world(typ, obj, c): 123 | labels_obj = c.pyapi.object_getattr_string(obj, "_labels") 124 | world_obj = c.pyapi.object_getattr_string(obj, "_world") 125 | world = cgutils.create_struct_proxy(typ)(c.context, c.builder) 126 | world.labels = c.unbox(types.ListType(label.label_type), labels_obj).value 127 | world.world = c.unbox(types.DictType(label.label_type, interval.interval_type), world_obj).value 128 | c.pyapi.decref(labels_obj) 129 | c.pyapi.decref(world_obj) 130 | is_error = cgutils.is_not_null(c.builder, c.pyapi.err_occurred()) 131 | return NativeValue(world._getvalue(), is_error=is_error) 132 | 133 | 134 | 135 | @box(WorldType) 136 | def box_world(typ, val, c): 137 | w = cgutils.create_struct_proxy(typ)(c.context, c.builder, value=val) 138 | class_obj = c.pyapi.unserialize(c.pyapi.serialize_object(World.make_world)) 139 | labels_obj = c.box(types.ListType(label.label_type), w.labels) 140 | world_obj = c.box(types.DictType(label.label_type, interval.interval_type), w.world) 141 | res = c.pyapi.call_function_objargs(class_obj, (labels_obj, world_obj)) 142 | c.pyapi.decref(labels_obj) 143 | c.pyapi.decref(world_obj) 144 | c.pyapi.decref(class_obj) 145 | return res 146 | -------------------------------------------------------------------------------- /pyreason/scripts/program/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/program/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/program/program.py: -------------------------------------------------------------------------------- 1 | from pyreason.scripts.interpretation.interpretation import Interpretation as Interpretation 2 | from pyreason.scripts.interpretation.interpretation_parallel import Interpretation as InterpretationParallel 3 | 4 | 5 | class Program: 6 | specific_node_labels = [] 7 | specific_edge_labels = [] 8 | 9 | def __init__(self, graph, facts_node, facts_edge, rules, ipl, annotation_functions, reverse_graph, atom_trace, save_graph_attributes_to_rule_trace, canonical, inconsistency_check, store_interpretation_changes, parallel_computing, update_mode, allow_ground_rules): 10 | self._graph = graph 11 | self._facts_node = facts_node 12 | self._facts_edge = facts_edge 13 | self._rules = rules 14 | self._ipl = ipl 15 | self._annotation_functions = annotation_functions 16 | self._reverse_graph = reverse_graph 17 | self._atom_trace = atom_trace 18 | self._save_graph_attributes_to_rule_trace = save_graph_attributes_to_rule_trace 19 | self._canonical = canonical 20 | self._inconsistency_check = inconsistency_check 21 | self._store_interpretation_changes = store_interpretation_changes 22 | self._parallel_computing = parallel_computing 23 | self._update_mode = update_mode 24 | self._allow_ground_rules = allow_ground_rules 25 | self.interp = None 26 | 27 | def reason(self, tmax, convergence_threshold, convergence_bound_threshold, verbose=True): 28 | self._tmax = tmax 29 | # Set up available labels 30 | Interpretation.specific_node_labels = self.specific_node_labels 31 | Interpretation.specific_edge_labels = self.specific_edge_labels 32 | 33 | # Instantiate correct interpretation class based on whether we parallelize the code or not. (We cannot parallelize with cache on) 34 | if self._parallel_computing: 35 | self.interp = InterpretationParallel(self._graph, self._ipl, self._annotation_functions, self._reverse_graph, self._atom_trace, self._save_graph_attributes_to_rule_trace, self._canonical, self._inconsistency_check, self._store_interpretation_changes, self._update_mode, self._allow_ground_rules) 36 | else: 37 | self.interp = Interpretation(self._graph, self._ipl, self._annotation_functions, self._reverse_graph, self._atom_trace, self._save_graph_attributes_to_rule_trace, self._canonical, self._inconsistency_check, self._store_interpretation_changes, self._update_mode, self._allow_ground_rules) 38 | self.interp.start_fp(self._tmax, self._facts_node, self._facts_edge, self._rules, verbose, convergence_threshold, convergence_bound_threshold) 39 | 40 | return self.interp 41 | 42 | def reason_again(self, tmax, restart, convergence_threshold, convergence_bound_threshold, facts_node, facts_edge, verbose=True): 43 | assert self.interp is not None, 'Call reason before calling reason again' 44 | if restart: 45 | self._tmax = tmax 46 | else: 47 | self._tmax = self.interp.time + tmax 48 | self.interp.start_fp(self._tmax, facts_node, facts_edge, self._rules, verbose, convergence_threshold, convergence_bound_threshold, again=True, restart=restart) 49 | 50 | return self.interp 51 | 52 | def reset_graph(self): 53 | self._graph = None 54 | self.interp = None 55 | 56 | def reset_rules(self): 57 | self._rules = None 58 | 59 | def reset_facts(self): 60 | self._facts_node = None 61 | self._facts_edge = None 62 | -------------------------------------------------------------------------------- /pyreason/scripts/query/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/query/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/query/query.py: -------------------------------------------------------------------------------- 1 | from pyreason.scripts.utils.query_parser import parse_query 2 | 3 | 4 | class Query: 5 | def __init__(self, query_text: str): 6 | """ 7 | PyReason query object which is parsed from a string of form: 8 | `pred(node)` or `pred(edge)` or `pred(node) : [l, u]` 9 | If bounds are not specified, they are set to [1, 1] by default. A tilde `~` before the predicate means that the bounds 10 | are inverted, i.e. [0, 0] for [1, 1] and vice versa. 11 | 12 | Queries can be used to analyze the graph and extract information about the graph after the reasoning process. 13 | Queries can also be used as input to the reasoner to filter the ruleset based which rules are applicable to the query. 14 | 15 | :param query_text: The query string of form described above 16 | """ 17 | self.__pred, self.__component, self.__comp_type, self.__bnd = parse_query(query_text) 18 | self.query_text = query_text 19 | 20 | def get_predicate(self): 21 | return self.__pred 22 | 23 | def get_component(self): 24 | return self.__component 25 | 26 | def get_component_type(self): 27 | return self.__comp_type 28 | 29 | def get_bounds(self): 30 | return self.__bnd 31 | 32 | def __str__(self): 33 | return self.query_text 34 | 35 | def __repr__(self): 36 | return self.query_text 37 | -------------------------------------------------------------------------------- /pyreason/scripts/rules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/rules/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/rules/rule.py: -------------------------------------------------------------------------------- 1 | import pyreason.scripts.utils.rule_parser as rule_parser 2 | 3 | 4 | class Rule: 5 | """ 6 | Example text: 7 | `'pred1(x,y) : [0.2, 1] <- pred2(a, b) : [1,1], pred3(b, c)'` 8 | 9 | 1. It is not possible to have weights for different clauses. Weights are 1 by default with bias 0 10 | """ 11 | def __init__(self, rule_text: str, name: str = None, infer_edges: bool = False, set_static: bool = False, custom_thresholds=None, weights=None): 12 | """ 13 | :param rule_text: The rule in text format 14 | :param name: The name of the rule. This will appear in the rule trace 15 | :param infer_edges: Whether to infer new edges after edge rule fires 16 | :param set_static: Whether to set the atom in the head as static if the rule fires. The bounds will no longer change 17 | :param custom_thresholds: A list of custom thresholds for the rule. If not specified, the default thresholds for ANY are used. It can be a list of 18 | size #of clauses or a map of clause index to threshold 19 | :param weights: A list of weights for the rule clauses. This is passed to an annotation function. If not specified, 20 | the weights array is a list of 1s with the length as number of clauses. 21 | """ 22 | self.rule = rule_parser.parse_rule(rule_text, name, custom_thresholds, infer_edges, set_static, weights) 23 | -------------------------------------------------------------------------------- /pyreason/scripts/rules/rule_internal.py: -------------------------------------------------------------------------------- 1 | class Rule: 2 | 3 | def __init__(self, rule_name, rule_type, target, head_variables, delta, clauses, bnd, thresholds, ann_fn, weights, edges, static): 4 | self._rule_name = rule_name 5 | self._type = rule_type 6 | self._target = target 7 | self._head_variables = head_variables 8 | self._delta = delta 9 | self._clauses = clauses 10 | self._bnd = bnd 11 | self._thresholds = thresholds 12 | self._ann_fn = ann_fn 13 | self._weights = weights 14 | self._edges = edges 15 | self._static = static 16 | 17 | def get_rule_name(self): 18 | return self._rule_name 19 | 20 | def set_rule_name(self, rule_name): 21 | self._rule_name = rule_name 22 | 23 | def get_rule_type(self): 24 | return self._type 25 | 26 | def get_target(self): 27 | return self._target 28 | 29 | def get_head_variables(self): 30 | return self._head_variables 31 | 32 | def get_delta(self): 33 | return self._delta 34 | 35 | def get_clauses(self): 36 | return self._clauses 37 | 38 | def set_clauses(self, clauses): 39 | self._clauses = clauses 40 | 41 | def get_bnd(self): 42 | return self._bnd 43 | 44 | def get_thresholds(self): 45 | return self._thresholds 46 | 47 | def set_thresholds(self, thresholds): 48 | self._thresholds = thresholds 49 | 50 | def get_annotation_function(self): 51 | return self._ann_fn 52 | 53 | def get_edges(self): 54 | return self._edges 55 | 56 | def get_weights(self): 57 | return self._weights 58 | 59 | def is_static(self): 60 | return self._static 61 | 62 | def __eq__(self, other): 63 | if not isinstance(other, Rule): 64 | return False 65 | clause_eq = [] 66 | other_clause_eq = [] 67 | for c in self._clauses: 68 | clause_eq.append((c[0], c[1], tuple(c[2]), c[3], c[4])) 69 | for c in other.get_clauses(): 70 | other_clause_eq.append((c[0], c[1], tuple(c[2]), c[3], c[4])) 71 | if self._rule_name == other.get_rule_name() and self._type == other.get_rule_type() and self._target == other.get_target() and self._head_variables == other.get_head_variables() and self._delta == other.get_delta() and tuple(clause_eq) == tuple(other_clause_eq) and self._bnd == other.get_bnd(): 72 | return True 73 | else: 74 | return False 75 | 76 | def __hash__(self): 77 | clause_hashes = [] 78 | for c in self._clauses: 79 | clause_hash = (c[0], c[1], tuple(c[2]), c[3], c[4]) 80 | clause_hashes.append(clause_hash) 81 | 82 | return hash((self._rule_name, self._type, self._target.get_value(), *self._head_variables, self._delta, *clause_hashes, self._bnd)) 83 | -------------------------------------------------------------------------------- /pyreason/scripts/threshold/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/threshold/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/threshold/threshold.py: -------------------------------------------------------------------------------- 1 | class Threshold: 2 | """ 3 | A class representing a threshold for a clause in a rule. 4 | 5 | Attributes: 6 | quantifier (str): The comparison operator, e.g., 'greater_equal', 'less', etc. 7 | quantifier_type (tuple): A tuple indicating the type of quantifier, e.g., ('number', 'total'). 8 | thresh (int): The numerical threshold value to compare against. 9 | 10 | Methods: 11 | to_tuple(): Converts the Threshold instance into a tuple compatible with numba types. 12 | """ 13 | 14 | def __init__(self, quantifier, quantifier_type, thresh): 15 | """ 16 | Initializes a Threshold instance. 17 | 18 | Args: 19 | quantifier (str): The comparison operator for the threshold. 20 | quantifier_type (tuple): The type of quantifier ('number' or 'percent', 'total' or 'available'). 21 | thresh (int): The numerical value for the threshold. 22 | """ 23 | 24 | if quantifier not in ("greater_equal", "greater", "less_equal", "less", "equal"): 25 | raise ValueError("Invalid quantifier") 26 | 27 | if quantifier_type[0] not in ("number", "percent") or quantifier_type[1] not in ("total", "available"): 28 | raise ValueError("Invalid quantifier type") 29 | 30 | self.quantifier = quantifier 31 | self.quantifier_type = quantifier_type 32 | self.thresh = thresh 33 | 34 | def to_tuple(self): 35 | """ 36 | Converts the Threshold instance into a tuple compatible with numba types. 37 | 38 | Returns: 39 | tuple: A tuple representation of the Threshold instance. 40 | """ 41 | return self.quantifier, self.quantifier_type, self.thresh 42 | -------------------------------------------------------------------------------- /pyreason/scripts/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-v2/pyreason/50a7753052dd6f0d0190289f2896c6f6698f80c0/pyreason/scripts/utils/__init__.py -------------------------------------------------------------------------------- /pyreason/scripts/utils/fact_parser.py: -------------------------------------------------------------------------------- 1 | import pyreason.scripts.numba_wrapper.numba_types.interval_type as interval 2 | 3 | 4 | def parse_fact(fact_text): 5 | f = fact_text.replace(' ', '') 6 | 7 | # Separate into predicate-component and bound. If there is no bound it means it's true 8 | if ':' in f: 9 | pred_comp, bound = f.split(':') 10 | else: 11 | pred_comp = f 12 | if pred_comp[0] == '~': 13 | bound = 'False' 14 | pred_comp = pred_comp[1:] 15 | else: 16 | bound = 'True' 17 | 18 | # Check if bound is a boolean or a list of floats 19 | bound = bound.lower() 20 | if bound == 'true': 21 | bound = interval.closed(1, 1) 22 | elif bound == 'false': 23 | bound = interval.closed(0, 0) 24 | else: 25 | bound = [float(b) for b in bound[1:-1].split(',')] 26 | bound = interval.closed(*bound) 27 | 28 | # Split the predicate and component 29 | idx = pred_comp.find('(') 30 | pred = pred_comp[:idx] 31 | component = pred_comp[idx + 1:-1] 32 | 33 | # Check if it is a node or edge fact 34 | if ',' in component: 35 | fact_type = 'edge' 36 | component = tuple(component.split(',')) 37 | else: 38 | fact_type = 'node' 39 | 40 | return pred, component, bound, fact_type 41 | -------------------------------------------------------------------------------- /pyreason/scripts/utils/filter.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | 4 | class Filter: 5 | def __init__(self, tmax): 6 | self.tmax = tmax 7 | 8 | def filter_and_sort_nodes(self, interpretation, labels, bound, sort_by='lower', descending=True): 9 | # Make use of rule trace in interpretation object to efficiently filter through data. 10 | 11 | # Initialize nested dict 12 | df = {} 13 | nodes = [] 14 | latest_changes = {} 15 | for t in range(self.tmax+1): 16 | df[t] = {} 17 | nodes.append({}) 18 | latest_changes[t] = {} 19 | 20 | # change contains the timestep, fp operation, component, label and interval 21 | # Keep only the latest/most recent changes. Since list is sequencial, whatever was earlier will be overwritten 22 | for change in interpretation.rule_trace_node: 23 | t, fp, comp, l, bnd = change 24 | latest_changes[t][(comp, l)] = bnd 25 | 26 | # Create a list that needs to be sorted. This contains only the latest changes 27 | list_to_be_sorted = [] 28 | for t, d in latest_changes.items(): 29 | for (comp, l), bnd in d.items(): 30 | list_to_be_sorted.append((bnd, t, comp, l)) 31 | 32 | # Sort the list 33 | reverse = True if descending else False 34 | if sort_by == 'lower': 35 | list_to_be_sorted.sort(key=lambda x: x[0].lower, reverse=reverse) 36 | elif sort_by == 'upper': 37 | list_to_be_sorted.sort(key=lambda x: x[0].upper, reverse=reverse) 38 | 39 | # Add sorted elements to df 40 | for i in list_to_be_sorted: 41 | bnd, t, comp, l = i 42 | df[t][(comp, l)] = bnd 43 | 44 | for t, d in df.items(): 45 | for (comp, l), bnd in d.items(): 46 | if l.get_value() in labels and bnd in bound: 47 | if comp not in nodes[t]: 48 | nodes[t][comp] = {lab:[0,1] for lab in labels} 49 | nodes[t][comp][l.get_value()] = [bnd.lower, bnd.upper] 50 | 51 | dataframes = [] 52 | for t in range(self.tmax+1): 53 | dataframe = pd.DataFrame.from_dict(nodes[t]).transpose() 54 | dataframe = dataframe.reset_index() 55 | if not dataframe.empty: 56 | dataframe.columns = ['component', *labels] 57 | else: 58 | dataframe = pd.DataFrame(columns=['component', *labels]) 59 | dataframes.append(dataframe) 60 | return dataframes 61 | 62 | def filter_and_sort_edges(self, interpretation, labels, bound, sort_by='lower', descending=True): 63 | # Make use of rule trace in interpretation object to efficiently filter through data. 64 | 65 | # Initialize nested dict 66 | df = {} 67 | edges = [] 68 | latest_changes = {} 69 | for t in range(self.tmax+1): 70 | df[t] = {} 71 | edges.append({}) 72 | latest_changes[t] = {} 73 | 74 | # change contains the timestep, fp operation, component, label and interval 75 | # Keep only the latest/most recent changes. Since list is sequential, whatever was earlier will be overwritten 76 | for change in interpretation.rule_trace_edge: 77 | t, fp, comp, l, bnd = change 78 | latest_changes[t][(comp, l)] = bnd 79 | 80 | # Create a list that needs to be sorted. This contains only the latest changes 81 | list_to_be_sorted = [] 82 | for t, d in latest_changes.items(): 83 | for (comp, l), bnd in d.items(): 84 | list_to_be_sorted.append((bnd, t, comp, l)) 85 | 86 | # Sort the list 87 | reverse = True if descending else False 88 | if sort_by == 'lower': 89 | list_to_be_sorted.sort(key=lambda x: x[0].lower, reverse=reverse) 90 | elif sort_by == 'upper': 91 | list_to_be_sorted.sort(key=lambda x: x[0].upper, reverse=reverse) 92 | 93 | # Add sorted elements to df 94 | for i in list_to_be_sorted: 95 | bnd, t, comp, l = i 96 | df[t][(comp, l)] = bnd 97 | 98 | for t, d in df.items(): 99 | for (comp, l), bnd in d.items(): 100 | if l.get_value() in labels and bnd in bound: 101 | if comp not in edges[t]: 102 | edges[t][comp] = {lab: [0, 1] for lab in labels} 103 | edges[t][comp][l.get_value()] = [bnd.lower, bnd.upper] 104 | 105 | dataframes = [] 106 | for t in range(self.tmax+1): 107 | dataframe = pd.DataFrame.from_dict(edges[t]).transpose() 108 | dataframe = dataframe.reset_index() 109 | if not dataframe.empty: 110 | dataframe.columns = ['source', 'target', *labels] 111 | dataframe['component'] = dataframe[['source', 'target']].apply(tuple, axis=1) 112 | dataframe.drop(columns=['source', 'target'], inplace=True) 113 | dataframe = dataframe[['component', *labels]] 114 | else: 115 | dataframe = pd.DataFrame(columns=['component', *labels]) 116 | dataframes.append(dataframe) 117 | return dataframes 118 | -------------------------------------------------------------------------------- /pyreason/scripts/utils/filter_ruleset.py: -------------------------------------------------------------------------------- 1 | def filter_ruleset(queries, rules): 2 | """ 3 | Filter the ruleset based on the queries provided. 4 | 5 | :param queries: List of Query objects 6 | :param rules: List of Rule objects 7 | :return: List of Rule objects that are applicable to the queries 8 | """ 9 | 10 | # Helper function to collect all rules that can support making a given rule true 11 | def applicable_rules_from_query(query): 12 | # Start with rules that match the query directly 13 | applicable = [] 14 | 15 | for rule in rules: 16 | # If the rule's target matches the query 17 | if query == rule.get_target(): 18 | # Add the rule to the applicable set 19 | applicable.append(rule) 20 | # Recursively check rules that can lead up to this rule 21 | for clause in rule.get_clauses(): 22 | # Find supporting rules with the clause as the target 23 | supporting_rules = applicable_rules_from_query(clause[1]) 24 | applicable.extend(supporting_rules) 25 | 26 | return applicable 27 | 28 | # Collect applicable rules for each query and eliminate duplicates 29 | filtered_rules = [] 30 | for q in queries: 31 | filtered_rules.extend(applicable_rules_from_query(q.get_predicate())) 32 | 33 | # Use set to avoid duplicates if a rule supports multiple queries 34 | return list(set(filtered_rules)) 35 | -------------------------------------------------------------------------------- /pyreason/scripts/utils/graphml_parser.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import numba 3 | 4 | import pyreason.scripts.numba_wrapper.numba_types.fact_node_type as fact_node 5 | import pyreason.scripts.numba_wrapper.numba_types.fact_edge_type as fact_edge 6 | import pyreason.scripts.numba_wrapper.numba_types.label_type as label 7 | import pyreason.scripts.numba_wrapper.numba_types.interval_type as interval 8 | 9 | 10 | class GraphmlParser: 11 | def __init__(self): 12 | self.graph = None 13 | self.non_fluent_facts = None 14 | 15 | def parse_graph(self, graph_path, reverse): 16 | self.graph = nx.read_graphml(graph_path) 17 | self.graph = nx.DiGraph(self.graph) 18 | if reverse: 19 | self.graph = self.graph.reverse() 20 | 21 | return self.graph 22 | 23 | def load_graph(self, graph): 24 | self.graph = nx.DiGraph(graph) 25 | return self.graph 26 | 27 | def parse_graph_attributes(self, static_facts): 28 | facts_node = numba.typed.List.empty_list(fact_node.fact_type) 29 | facts_edge = numba.typed.List.empty_list(fact_edge.fact_type) 30 | specific_node_labels = numba.typed.Dict.empty(key_type=label.label_type, value_type=numba.types.ListType(numba.types.string)) 31 | specific_edge_labels = numba.typed.Dict.empty(key_type=label.label_type, value_type=numba.types.ListType(numba.types.Tuple((numba.types.string, numba.types.string)))) 32 | for n in self.graph.nodes: 33 | for key, value in self.graph.nodes[n].items(): 34 | # IF attribute is a float or int and it is less than 1, then make it a bound, else make it a label 35 | if (isinstance(value, (float, int)) and 1 >= value >= 0) or ( 36 | isinstance(value, str) and value.replace('.', '').isdigit() and 1 >= float(value) >= 0): 37 | l = str(key) 38 | l_bnd = float(value) 39 | u_bnd = 1 40 | else: 41 | l = f'{key}-{value}' 42 | l_bnd = 1 43 | u_bnd = 1 44 | if isinstance(value, str): 45 | bnd_str = value.split(',') 46 | if len(bnd_str) == 2: 47 | try: 48 | low = int(bnd_str[0]) 49 | up = int(bnd_str[1]) 50 | if 1 >= low >= 0 and 1 >= up >= 0: 51 | l_bnd = low 52 | u_bnd = up 53 | l = str(key) 54 | except: 55 | pass 56 | 57 | if label.Label(l) not in specific_node_labels.keys(): 58 | specific_node_labels[label.Label(l)] = numba.typed.List.empty_list(numba.types.string) 59 | specific_node_labels[label.Label(l)].append(n) 60 | f = fact_node.Fact('graph-attribute-fact', n, label.Label(l), interval.closed(l_bnd, u_bnd), 0, 0, static=static_facts) 61 | facts_node.append(f) 62 | for e in self.graph.edges: 63 | for key, value in self.graph.edges[e].items(): 64 | # IF attribute is a float or int and it is less than 1, then make it a bound, else make it a label 65 | if (isinstance(value, (float, int)) and 1 >= value >= 0) or ( 66 | isinstance(value, str) and value.replace('.', '').isdigit() and 1 >= float(value) >= 0): 67 | l = str(key) 68 | l_bnd = float(value) 69 | u_bnd = 1 70 | else: 71 | l = f'{key}-{value}' 72 | l_bnd = 1 73 | u_bnd = 1 74 | if isinstance(value, str): 75 | bnd_str = value.split(',') 76 | if len(bnd_str) == 2: 77 | try: 78 | low = int(bnd_str[0]) 79 | up = int(bnd_str[1]) 80 | if 1 >= low >= 0 and 1 >= up >= 0: 81 | l_bnd = low 82 | u_bnd = up 83 | l = str(key) 84 | except: 85 | pass 86 | 87 | if label.Label(l) not in specific_edge_labels.keys(): 88 | specific_edge_labels[label.Label(l)] = numba.typed.List.empty_list(numba.types.Tuple((numba.types.string, numba.types.string))) 89 | specific_edge_labels[label.Label(l)].append((e[0], e[1])) 90 | f = fact_edge.Fact('graph-attribute-fact', (e[0], e[1]), label.Label(l), interval.closed(l_bnd, u_bnd), 0, 0, static=static_facts) 91 | facts_edge.append(f) 92 | 93 | return facts_node, facts_edge, specific_node_labels, specific_edge_labels -------------------------------------------------------------------------------- /pyreason/scripts/utils/output.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | import pandas as pd 4 | 5 | 6 | class Output: 7 | def __init__(self, timestamp, clause_map=None): 8 | self.timestamp = timestamp 9 | self.clause_map = clause_map 10 | self.rule_trace_node = None 11 | self.rule_trace_edge = None 12 | 13 | def _parse_internal_rule_trace(self, interpretation): 14 | header_node = ['Time', 'Fixed-Point-Operation', 'Node', 'Label', 'Old Bound', 'New Bound', 'Occurred Due To'] 15 | 16 | # Nodes rule trace 17 | data = [] 18 | max_j = -1 19 | 20 | for i, r in enumerate(interpretation.rule_trace_node): 21 | row = [r[0], r[1], r[2], r[3]._value, '-', r[4].to_str(), '-'] 22 | if interpretation.atom_trace: 23 | qn, qe, old_bnd, name = interpretation.rule_trace_node_atoms[i] 24 | row[4] = old_bnd.to_str() 25 | # Go through all the changes in the rule trace 26 | # len(qn) = len(qe) = num of clauses in rule that was used 27 | row[6] = name 28 | 29 | # Go through each clause 30 | for j in range(len(qn)): 31 | max_j = max(j, max_j) 32 | if len(qe[j]) == 0: 33 | # Node clause 34 | row.append(list(qn[j])) 35 | elif len(qn[j]) == 0: 36 | # Edge clause 37 | row.append(list(qe[j])) 38 | 39 | data.append(row) 40 | 41 | # Add Clause-num to header 42 | if interpretation.atom_trace and max_j != -1: 43 | for i in range(1, max_j + 2): 44 | header_node.append(f'Clause-{i}') 45 | 46 | # Store the trace in a DataFrame 47 | self.rule_trace_node = pd.DataFrame(data, columns=header_node) 48 | 49 | header_edge = ['Time', 'Fixed-Point-Operation', 'Edge', 'Label', 'Old Bound', 'New Bound', 'Occurred Due To'] 50 | 51 | # Edges rule trace 52 | data = [] 53 | max_j = -1 54 | 55 | for i, r in enumerate(interpretation.rule_trace_edge): 56 | row = [r[0], r[1], r[2], r[3]._value, '-', r[4].to_str(), '-'] 57 | if interpretation.atom_trace: 58 | qn, qe, old_bnd, name = interpretation.rule_trace_edge_atoms[i] 59 | row[4] = old_bnd.to_str() 60 | # Go through all the changes in the rule trace 61 | # len(qn) = num of clauses in rule that was used 62 | row[6] = name 63 | 64 | # Go through each clause 65 | for j in range(len(qn)): 66 | max_j = max(j, max_j) 67 | if len(qe[j]) == 0: 68 | # Node clause 69 | row.append(list(qn[j])) 70 | elif len(qn[j]) == 0: 71 | # Edge clause 72 | row.append(list(qe[j])) 73 | 74 | data.append(row) 75 | 76 | # Add Clause-num to header 77 | if interpretation.atom_trace and max_j != -1: 78 | for i in range(1, max_j + 2): 79 | header_edge.append(f'Clause-{i}') 80 | 81 | # Store the trace in a DataFrame 82 | self.rule_trace_edge = pd.DataFrame(data, columns=header_edge) 83 | 84 | # Now do the reordering 85 | if self.clause_map is not None: 86 | offset = 7 87 | columns_to_reorder_node = header_node[offset:] 88 | columns_to_reorder_edge = header_edge[offset:] 89 | self.rule_trace_node = self.rule_trace_node.apply(self._reorder_row, axis=1, map_dict=self.clause_map, columns_to_reorder=columns_to_reorder_node) 90 | self.rule_trace_edge = self.rule_trace_edge.apply(self._reorder_row, axis=1, map_dict=self.clause_map, columns_to_reorder=columns_to_reorder_edge) 91 | 92 | def save_rule_trace(self, interpretation, folder='./'): 93 | if self.rule_trace_node is None and self.rule_trace_edge is None: 94 | self._parse_internal_rule_trace(interpretation) 95 | 96 | path_nodes = os.path.join(folder, f'rule_trace_nodes_{self.timestamp}.csv') 97 | path_edges = os.path.join(folder, f'rule_trace_edges_{self.timestamp}.csv') 98 | self.rule_trace_node.to_csv(path_nodes, index=False) 99 | self.rule_trace_edge.to_csv(path_edges, index=False) 100 | 101 | def get_rule_trace(self, interpretation): 102 | if self.rule_trace_node is None and self.rule_trace_edge is None: 103 | self._parse_internal_rule_trace(interpretation) 104 | 105 | return self.rule_trace_node, self.rule_trace_edge 106 | 107 | @staticmethod 108 | def _reorder_row(row, map_dict, columns_to_reorder): 109 | if row['Occurred Due To'] in map_dict: 110 | original_values = row[columns_to_reorder].values 111 | new_values = [None] * len(columns_to_reorder) 112 | for orig_pos, target_pos in map_dict[row['Occurred Due To']].items(): 113 | new_values[target_pos] = original_values[orig_pos] 114 | for i, col in enumerate(columns_to_reorder): 115 | row[col] = new_values[i] 116 | return row 117 | -------------------------------------------------------------------------------- /pyreason/scripts/utils/plotter.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import matplotlib.pyplot as plt 3 | import seaborn as sns 4 | 5 | 6 | def main(): 7 | sns.set() 8 | smooth = False 9 | # data_path_jit = f'/home/dyuman/Documents/Mancalog/profiling/profile_jit.csv' 10 | # data_path_old = f'/home/dyuman/Documents/Mancalog/profiling/profile_old.csv' 11 | # data_path_oldest = f'/home/dyuman/Documents/Mancalog/profiling/profile_oldest.csv' 12 | # data = f'/home/dyuman/Downloads/memory.csv' 13 | 14 | x_axis_title = 'Number of Nodes (density=4.10e-4)' 15 | y_axis_title = 'Runtime (s)' 16 | title = 'Number of Nodes vs Runtime' 17 | 18 | # headers = ['Timesteps', 'Memory', 'Memory Old'] 19 | # df_jit = pd.read_csv(data_path_jit, names=headers, header=None) 20 | # df_old = pd.read_csv(data_path_old, names=headers, header=None) 21 | # df_oldest = pd.read_csv(data_path_oldest, names=headers, header=None) 22 | # df = pd.read_csv(data, names=headers, header=None) 23 | # x_jit = df_jit['Timesteps'] 24 | # y_jit = df_jit['Time'] 25 | # x_old = df_old['Timesteps'] 26 | # y_old = df_old['Time'] 27 | # x_oldest = df_oldest['Timesteps'] 28 | # y_oldest = df_oldest['Time'] 29 | # x = df['Timesteps'] 30 | # y1 = df['Memory'] 31 | # y2 = df['Memory Old'] 32 | 33 | x = [1000, 2000, 5000, 10000] 34 | y2 = [0.35, 0.40, 1.35, 4.25] 35 | y5 = [0.43, 0.49, 1.76, 6.11] 36 | y15 = [0.40, 0.73, 3.09, 11.20] 37 | 38 | # if smooth: 39 | # y_jit = pd.Series(y_jit).rolling(15, min_periods=1).mean() 40 | # y_old = pd.Series(y_old).rolling(15, min_periods=1).mean() 41 | # y_oldest = pd.Series(y_oldest).rolling(15, min_periods=1).mean() 42 | 43 | # sns.relplot(data=df, x =x_axis_title, y=y_axis_title, kind = 'line', hue = 'type', palette = ['red', 'blue']) 44 | # ax = sns.relplot(data=df, kind = 'line', ci=0) 45 | # plt.plot(x_jit, y_jit, label='accelerated with numba for CPU') 46 | # plt.plot(x_old, y_old, label='first optimized version') 47 | # plt.plot(x_oldest, y_oldest, label='original version') 48 | plt.plot(x, y2, linestyle='dotted', marker='^', label='2 Timesteps') 49 | plt.plot(x, y5, linestyle='dotted', marker='s', label='5 Timesteps') 50 | plt.plot(x, y15, linestyle='dotted', marker='o', label='15 Timesteps') 51 | plt.legend() 52 | plt.title(title, fontsize=20) 53 | plt.xlabel(x_axis_title, fontsize=13) 54 | plt.ylabel(y_axis_title, fontsize=13) 55 | 56 | # ax = sns.lineplot(x=x, y=y, ci=95) 57 | # ax = sns.lineplot(x=x_jit, y=y_jit, label='') 58 | # ax = sns.lineplot(x=x, y=y, label='') 59 | # ax = sns.lineplot(x=x, y=y, label='') 60 | 61 | 62 | # ax.axes.set_title(title, fontsize=18) 63 | # ax.set_xlabel(x_axis_title, fontsize=13) 64 | # ax.set_ylabel(y_axis_title, fontsize=13) 65 | # plt.show() 66 | if smooth: 67 | plt.savefig(f'timesteps_vs_time_smooth.png') 68 | else: 69 | plt.savefig(f'timesteps_vs_memory.png') 70 | 71 | 72 | if __name__ == '__main__': 73 | main() -------------------------------------------------------------------------------- /pyreason/scripts/utils/query_parser.py: -------------------------------------------------------------------------------- 1 | import pyreason.scripts.numba_wrapper.numba_types.interval_type as interval 2 | import pyreason.scripts.numba_wrapper.numba_types.label_type as label 3 | 4 | 5 | def parse_query(query: str): 6 | query = query.replace(' ', '') 7 | 8 | if ':' in query: 9 | pred_comp, bounds = query.split(':') 10 | bounds = bounds.replace('[', '').replace(']', '') 11 | l, u = bounds.split(',') 12 | l, u = float(l), float(u) 13 | else: 14 | if query[0] == '~': 15 | pred_comp = query[1:] 16 | l, u = 0, 0 17 | else: 18 | pred_comp = query 19 | l, u = 1, 1 20 | 21 | bnd = interval.closed(l, u) 22 | 23 | # Split predicate and component 24 | idx = pred_comp.find('(') 25 | pred = label.Label(pred_comp[:idx]) 26 | component = pred_comp[idx + 1:-1] 27 | 28 | if ',' in component: 29 | component = tuple(component.split(',')) 30 | comp_type = 'edge' 31 | else: 32 | comp_type = 'node' 33 | 34 | return pred, component, comp_type, bnd 35 | -------------------------------------------------------------------------------- /pyreason/scripts/utils/reorder_clauses.py: -------------------------------------------------------------------------------- 1 | import numba 2 | import pyreason.scripts.numba_wrapper.numba_types.label_type as label 3 | import pyreason.scripts.numba_wrapper.numba_types.interval_type as interval 4 | 5 | 6 | def reorder_clauses(rule): 7 | # Go through all clauses in the rule and re-order them if necessary 8 | # It is faster for grounding to have node clauses first and then edge clauses 9 | # Move all the node clauses to the front of the list 10 | reordered_clauses = numba.typed.List.empty_list(numba.types.Tuple((numba.types.string, label.label_type, numba.types.ListType(numba.types.string), interval.interval_type, numba.types.string))) 11 | reordered_thresholds = numba.typed.List.empty_list(numba.types.Tuple((numba.types.string, numba.types.UniTuple(numba.types.string, 2), numba.types.float64))) 12 | node_clauses = [] 13 | edge_clauses = [] 14 | reordered_clauses_map = {} 15 | 16 | for index, clause in enumerate(rule.get_clauses()): 17 | if clause[0] == 'node': 18 | node_clauses.append((index, clause)) 19 | else: 20 | edge_clauses.append((index, clause)) 21 | 22 | thresholds = rule.get_thresholds() 23 | for new_index, (original_index, clause) in enumerate(node_clauses + edge_clauses): 24 | reordered_clauses.append(clause) 25 | reordered_thresholds.append(thresholds[original_index]) 26 | reordered_clauses_map[new_index] = original_index 27 | 28 | rule.set_clauses(reordered_clauses) 29 | rule.set_thresholds(reordered_thresholds) 30 | return rule, reordered_clauses_map 31 | -------------------------------------------------------------------------------- /pyreason/scripts/utils/visuals.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Visualizing graphs with Networkx 3 | Author: Kaustuv Mukherji 4 | Initially written: 09-27-2022 5 | Last updated: 12-04-2022 6 | ''' 7 | 8 | import networkx as nx 9 | import matplotlib 10 | import matplotlib.pyplot as plt 11 | import pandas as pd 12 | from textwrap import wrap 13 | 14 | def get_subgraph(whole_graph, node_list): 15 | return nx.subgraph(whole_graph, node_list) 16 | 17 | def make_visuals(graph_data, nodelist): 18 | pos_g=nx.kamada_kawai_layout(graph_data) 19 | plt.figure() 20 | color_map = [] 21 | for node in list(graph_data.nodes): 22 | if node in nodelist: 23 | color_map.append('red') 24 | else: 25 | color_map.append('green') 26 | labels_g=nx.get_node_attributes(graph_data, "name") 27 | nx.draw(graph_data, pos=pos_g, node_color=color_map, node_size=100, font_size=10, font_color='DarkBlue', with_labels=True, labels=labels_g) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | networkx 2 | pyyaml 3 | pandas 4 | numba==0.59.1 5 | numpy==1.26.4 6 | memory_profiler 7 | pytest 8 | setuptools_scm 9 | 10 | sphinx_rtd_theme 11 | sphinx 12 | sphinx-autopackagesummary 13 | sphinx-autoapi 14 | -------------------------------------------------------------------------------- /run_on_agave.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #------------------------------------------------------------------------- 4 | #SBATCH -N 1 # number of nodes 5 | #SBATCH -n 45 # number of "tasks" (default: allocates 1 core per task) 6 | #SBATCH -t 0-02:00:00 # time in d-hh:mm:ss 7 | #SBATCH -p htc # partition 8 | #SBATCH -o ./jobs/slurm.%j.out # file to save job's STDOUT (%j = JobId) 9 | #SBATCH -e ./jobs/slurm.%j.err # file to save job's STDERR (%j = JobId) 10 | #SBATCH --mail-type=END,FAIL # Send an e-mail when a job stops, or fails 11 | #SBATCH --mail-user=%u@asu.edu # Mail-to address 12 | #SBATCH --export=NONE # Purge the job-submitting shell environment 13 | #------------------------------------------------------------------------- 14 | 15 | 16 | #------------------------------------------------------------------------- 17 | # Please change the following variables based on your needs 18 | timesteps=2 19 | graph_path=~/Documents/honda/JP3854600008_honda.graphml 20 | rules_yaml_path=pyreason/examples/example_yamls/rules.yaml 21 | facts_yaml_path=pyreason/examples/example_yamls/facts.yaml 22 | labels_yaml_path=pyreason/examples/example_yamls/labels.yaml 23 | ipl_yaml_path=pyreason/examples/example_yamls/ipl.yaml 24 | output_file_name=pyreason_output 25 | #------------------------------------------------------------------------- 26 | 27 | 28 | #------------------------------------------------------------------------- 29 | # Initialize conda environment 30 | module load anaconda/py3 31 | echo Checking if PYREASON conda environment exists 32 | if conda env list | grep ".*PYREASON.*" >/dev/null 2>&1 33 | then 34 | echo PYREASON environment exists 35 | source activate PYREASON 36 | else 37 | echo Creating PYREASON conda environment 38 | conda create -n PYREASON python=3.8 39 | source activate PYREASON 40 | echo Installing necessary packages 41 | pip install -r requirements.txt 42 | fi 43 | 44 | 45 | # Run pyreason 46 | python3 -u -m pyreason.scripts.diffuse --graph_path $graph_path --timesteps $timesteps --rules $rules_yaml_path --facts $facts_yaml_path --labels $labels_yaml_path --ipl $ipl_yaml_path --output_to_file --output_file $output_file_name 47 | #------------------------------------------------------------------------- 48 | 49 | 50 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | # Read the contents of README file 4 | from pathlib import Path 5 | 6 | this_directory = Path(__file__).parent 7 | long_description = (this_directory / "README.md").read_text(encoding='UTF-8') 8 | 9 | setup( 10 | name='pyreason', 11 | version='3.0.4', 12 | author='Dyuman Aditya', 13 | author_email='dyuman.aditya@gmail.com', 14 | description='An explainable inference software supporting annotated, real valued, graph based and temporal logic', 15 | long_description=long_description, 16 | long_description_content_type='text/markdown', 17 | url='https://github.com/lab-v2/pyreason', 18 | license='BSD 3-clause', 19 | project_urls={ 20 | 'Bug Tracker': 'https://github.com/lab-v2/pyreason/issues', 21 | 'Repository': 'https://github.com/lab-v2/pyreason' 22 | }, 23 | classifiers=[ 24 | "Programming Language :: Python :: 3", 25 | "License :: OSI Approved :: BSD License", 26 | "Operating System :: OS Independent" 27 | ], 28 | python_requires='>3.6', 29 | install_requires=[ 30 | 'networkx', 31 | 'pyyaml', 32 | 'pandas', 33 | 'numba', 34 | 'numpy', 35 | 'memory_profiler', 36 | 'pytest' 37 | ], 38 | use_scm_version=True, 39 | setup_requires=['setuptools_scm'], 40 | packages=find_packages(), 41 | include_package_data=True 42 | ) 43 | -------------------------------------------------------------------------------- /tests/friends_graph.graphml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 1 13 | 14 | 15 | 1 16 | 17 | 18 | 1 19 | 20 | 21 | 1 22 | 23 | 24 | 1 25 | 26 | 27 | 1 28 | 29 | 30 | 1 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/group_chat_graph.graphml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 1 12 | 13 | 14 | 1 15 | 16 | 17 | 1 18 | 19 | 20 | 1 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/knowledge_graph_test_subset.graphml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 1 17 | 18 | 19 | 1 20 | 21 | 22 | 1 23 | 24 | 25 | 1 26 | 27 | 28 | 1 29 | 30 | 31 | 1 32 | 33 | 34 | 1 35 | 36 | 37 | 1 38 | 39 | 40 | 1 41 | 42 | 43 | 1 44 | 45 | 46 | 1 47 | 48 | 49 | 1 50 | 51 | 52 | 1 53 | 54 | 55 | 1 56 | 57 | 58 | 1 59 | 60 | 61 | 1 62 | 63 | 64 | 1 65 | 66 | 67 | 1 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /tests/test_annotation_function.py: -------------------------------------------------------------------------------- 1 | # Test if annotation functions work 2 | import pyreason as pr 3 | import numba 4 | import numpy as np 5 | 6 | 7 | @numba.njit 8 | def probability_func(annotations, weights): 9 | prob_A = annotations[0][0].lower 10 | prob_B = annotations[1][0].lower 11 | union_prob = prob_A + prob_B 12 | union_prob = np.round(union_prob, 3) 13 | return union_prob, 1 14 | 15 | 16 | def test_annotation_function(): 17 | # Reset PyReason 18 | pr.reset() 19 | pr.reset_rules() 20 | 21 | pr.settings.allow_ground_rules = True 22 | 23 | pr.add_fact(pr.Fact('P(A) : [0.01, 1]')) 24 | pr.add_fact(pr.Fact('P(B) : [0.2, 1]')) 25 | pr.add_annotation_function(probability_func) 26 | pr.add_rule(pr.Rule('union_probability(A, B):probability_func <- P(A):[0, 1], P(B):[0, 1]', infer_edges=True)) 27 | 28 | interpretation = pr.reason(timesteps=1) 29 | 30 | dataframes = pr.filter_and_sort_edges(interpretation, ['union_probability']) 31 | for t, df in enumerate(dataframes): 32 | print(f'TIMESTEP - {t}') 33 | print(df) 34 | print() 35 | 36 | assert interpretation.query(pr.Query('union_probability(A, B) : [0.21, 1]')), 'Union probability should be 0.21' 37 | -------------------------------------------------------------------------------- /tests/test_classifier.py: -------------------------------------------------------------------------------- 1 | # Test cases for classifier integration with pyreason 2 | import pyreason as pr 3 | import torch 4 | import torch.nn as nn 5 | 6 | 7 | def test_classifier_integration(): 8 | # Reset PyReason 9 | pr.reset() 10 | pr.reset_rules() 11 | pr.reset_settings() 12 | 13 | # Create a dummy PyTorch model: input size 10, output 3 classes. 14 | model = nn.Linear(10, 3) 15 | 16 | # Define class names for the output classes. 17 | class_names = ["cat", "dog", "rabbit"] 18 | 19 | # Create integration options. 20 | # Only probabilities exceeding 0.6 will be considered. 21 | # For those, if set_lower_bound is True, lower bound becomes 0.95; if set_upper_bound is False, upper bound is forced to 1. 22 | interface_options = pr.ModelInterfaceOptions( 23 | threshold=0.4, 24 | set_lower_bound=True, 25 | set_upper_bound=False, 26 | snap_value=0.95 27 | ) 28 | 29 | # Create an instance of LogicIntegratedClassifier. 30 | logic_classifier = pr.LogicIntegratedClassifier(model, class_names, model_name="classifier", 31 | interface_options=interface_options) 32 | 33 | # Create a dummy input tensor with 10 features. 34 | input_tensor = torch.rand(1, 10) 35 | 36 | # Set time bounds for the facts. 37 | t1 = 0 38 | t2 = 0 39 | 40 | # Run the forward pass to get the model output and the corresponding PyReason facts. 41 | output, probabilities, facts = logic_classifier(input_tensor, t1, t2) 42 | 43 | # Assert that the output is a tensor. 44 | assert isinstance(output, torch.Tensor), "The model output should be a torch.Tensor" 45 | # Assert that we have one fact per class. 46 | assert len(facts) == len(class_names), "Expected one fact per class" 47 | 48 | # Print results for visual inspection. 49 | print('Logits', output) 50 | print('Probabilities', probabilities) 51 | print("\nGenerated PyReason Facts:") 52 | for fact in facts: 53 | print(fact) 54 | -------------------------------------------------------------------------------- /tests/test_custom_thresholds.py: -------------------------------------------------------------------------------- 1 | # Test if the simple program works with thresholds defined 2 | import pyreason as pr 3 | from pyreason import Threshold 4 | 5 | 6 | def test_custom_thresholds(): 7 | # Reset PyReason 8 | pr.reset() 9 | pr.reset_rules() 10 | 11 | # Modify the paths based on where you've stored the files we made above 12 | graph_path = "./tests/group_chat_graph.graphml" 13 | 14 | # Modify pyreason settings to make verbose 15 | pr.reset_settings() 16 | pr.settings.verbose = True # Print info to screen 17 | 18 | # Load all the files into pyreason 19 | pr.load_graphml(graph_path) 20 | 21 | # add custom thresholds 22 | user_defined_thresholds = [ 23 | Threshold("greater_equal", ("number", "total"), 1), 24 | Threshold("greater_equal", ("percent", "total"), 100), 25 | ] 26 | 27 | pr.add_rule( 28 | pr.Rule( 29 | "ViewedByAll(y) <- HaveAccess(x,y), Viewed(x)", 30 | "viewed_by_all_rule", 31 | custom_thresholds=user_defined_thresholds, 32 | ) 33 | ) 34 | 35 | pr.add_fact(pr.Fact("Viewed(Zach)", "seen-fact-zach", 0, 3)) 36 | pr.add_fact(pr.Fact("Viewed(Justin)", "seen-fact-justin", 0, 3)) 37 | pr.add_fact(pr.Fact("Viewed(Michelle)", "seen-fact-michelle", 1, 3)) 38 | pr.add_fact(pr.Fact("Viewed(Amy)", "seen-fact-amy", 2, 3)) 39 | 40 | # Run the program for three timesteps to see the diffusion take place 41 | interpretation = pr.reason(timesteps=3) 42 | 43 | # Display the changes in the interpretation for each timestep 44 | dataframes = pr.filter_and_sort_nodes(interpretation, ["ViewedByAll"]) 45 | for t, df in enumerate(dataframes): 46 | print(f"TIMESTEP - {t}") 47 | print(df) 48 | print() 49 | 50 | assert ( 51 | len(dataframes[0]) == 0 52 | ), "At t=0 the TextMessage should not have been ViewedByAll" 53 | assert ( 54 | len(dataframes[2]) == 1 55 | ), "At t=2 the TextMessage should have been ViewedByAll" 56 | 57 | # TextMessage should be ViewedByAll in t=2 58 | assert "TextMessage" in dataframes[2]["component"].values and dataframes[2].iloc[ 59 | 0 60 | ].ViewedByAll == [ 61 | 1, 62 | 1, 63 | ], "TextMessage should have ViewedByAll bounds [1,1] for t=2 timesteps" 64 | -------------------------------------------------------------------------------- /tests/test_hello_world.py: -------------------------------------------------------------------------------- 1 | # Test if the simple hello world program works 2 | import pyreason as pr 3 | import faulthandler 4 | 5 | 6 | def test_hello_world(): 7 | # Reset PyReason 8 | pr.reset() 9 | pr.reset_rules() 10 | pr.reset_settings() 11 | 12 | # Modify the paths based on where you've stored the files we made above 13 | graph_path = './tests/friends_graph.graphml' 14 | 15 | # Modify pyreason settings to make verbose 16 | pr.settings.verbose = True # Print info to screen 17 | # pr.settings.optimize_rules = False # Disable rule optimization for debugging 18 | 19 | # Load all the files into pyreason 20 | pr.load_graphml(graph_path) 21 | pr.add_rule(pr.Rule('popular(x) <-1 popular(y), Friends(x,y), owns(y,z), owns(x,z)', 'popular_rule')) 22 | pr.add_fact(pr.Fact('popular(Mary)', 'popular_fact', 0, 2)) 23 | 24 | # Run the program for two timesteps to see the diffusion take place 25 | faulthandler.enable() 26 | interpretation = pr.reason(timesteps=2) 27 | 28 | # Display the changes in the interpretation for each timestep 29 | dataframes = pr.filter_and_sort_nodes(interpretation, ['popular']) 30 | for t, df in enumerate(dataframes): 31 | print(f'TIMESTEP - {t}') 32 | print(df) 33 | print() 34 | 35 | assert len(dataframes[0]) == 1, 'At t=0 there should be one popular person' 36 | assert len(dataframes[1]) == 2, 'At t=1 there should be two popular people' 37 | assert len(dataframes[2]) == 3, 'At t=2 there should be three popular people' 38 | 39 | # Mary should be popular in all three timesteps 40 | assert 'Mary' in dataframes[0]['component'].values and dataframes[0].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=0 timesteps' 41 | assert 'Mary' in dataframes[1]['component'].values and dataframes[1].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=1 timesteps' 42 | assert 'Mary' in dataframes[2]['component'].values and dataframes[2].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=2 timesteps' 43 | 44 | # Justin should be popular in timesteps 1, 2 45 | assert 'Justin' in dataframes[1]['component'].values and dataframes[1].iloc[1].popular == [1, 1], 'Justin should have popular bounds [1,1] for t=1 timesteps' 46 | assert 'Justin' in dataframes[2]['component'].values and dataframes[2].iloc[2].popular == [1, 1], 'Justin should have popular bounds [1,1] for t=2 timesteps' 47 | 48 | # John should be popular in timestep 3 49 | assert 'John' in dataframes[2]['component'].values and dataframes[2].iloc[1].popular == [1, 1], 'John should have popular bounds [1,1] for t=2 timesteps' 50 | 51 | -------------------------------------------------------------------------------- /tests/test_hello_world_parallel.py: -------------------------------------------------------------------------------- 1 | # Test if the simple hello world program works 2 | import pyreason as pr 3 | 4 | 5 | def test_hello_world_parallel(): 6 | # Reset PyReason 7 | pr.reset() 8 | pr.reset_rules() 9 | 10 | # Modify the paths based on where you've stored the files we made above 11 | graph_path = './tests/friends_graph.graphml' 12 | 13 | # Modify pyreason settings to make verbose 14 | pr.reset_settings() 15 | pr.settings.verbose = True # Print info to screen 16 | 17 | # Load all the files into pyreason 18 | pr.load_graphml(graph_path) 19 | pr.add_rule(pr.Rule('popular(x) <-1 popular(y), Friends(x,y), owns(y,z), owns(x,z)', 'popular_rule')) 20 | pr.add_fact(pr.Fact('popular(Mary)', 'popular_fact', 0, 2)) 21 | 22 | # Run the program for two timesteps to see the diffusion take place 23 | interpretation = pr.reason(timesteps=2) 24 | 25 | # Display the changes in the interpretation for each timestep 26 | dataframes = pr.filter_and_sort_nodes(interpretation, ['popular']) 27 | for t, df in enumerate(dataframes): 28 | print(f'TIMESTEP - {t}') 29 | print(df) 30 | print() 31 | 32 | assert len(dataframes[0]) == 1, 'At t=0 there should be one popular person' 33 | assert len(dataframes[1]) == 2, 'At t=1 there should be two popular people' 34 | assert len(dataframes[2]) == 3, 'At t=2 there should be three popular people' 35 | 36 | # Mary should be popular in all three timesteps 37 | assert 'Mary' in dataframes[0]['component'].values and dataframes[0].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=0 timesteps' 38 | assert 'Mary' in dataframes[1]['component'].values and dataframes[1].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=1 timesteps' 39 | assert 'Mary' in dataframes[2]['component'].values and dataframes[2].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=2 timesteps' 40 | 41 | # Justin should be popular in timesteps 1, 2 42 | assert 'Justin' in dataframes[1]['component'].values and dataframes[1].iloc[1].popular == [1, 1], 'Justin should have popular bounds [1,1] for t=1 timesteps' 43 | assert 'Justin' in dataframes[2]['component'].values and dataframes[2].iloc[2].popular == [1, 1], 'Justin should have popular bounds [1,1] for t=2 timesteps' 44 | 45 | # John should be popular in timestep 3 46 | assert 'John' in dataframes[2]['component'].values and dataframes[2].iloc[1].popular == [1, 1], 'John should have popular bounds [1,1] for t=2 timesteps' 47 | -------------------------------------------------------------------------------- /tests/test_num_ga.py: -------------------------------------------------------------------------------- 1 | # Test if the simple hello world program works 2 | import pyreason as pr 3 | 4 | 5 | def test_num_ga(): 6 | graph_path = './tests/knowledge_graph_test_subset.graphml' 7 | pr.reset() 8 | pr.reset_rules() 9 | # Modify pyreason settings to make verbose and to save the rule trace to a file 10 | pr.settings.verbose = True 11 | pr.settings.atom_trace = True 12 | pr.settings.canonical = True 13 | pr.settings.inconsistency_check = False 14 | pr.settings.static_graph_facts = False 15 | pr.settings.output_to_file = False 16 | pr.settings.store_interpretation_changes = True 17 | pr.settings.save_graph_attributes_to_trace = True 18 | 19 | # Load all the files into pyreason 20 | pr.load_graphml(graph_path) 21 | pr.add_rule(pr.Rule('isConnectedTo(A, Y) <-1 isConnectedTo(Y, B), Amsterdam_Airport_Schiphol(B), Vnukovo_International_Airport(A)', 'connected_rule_1', infer_edges=True)) 22 | # pr.add_fact(pr.Fact('dummy(Riga_International_Airport): [0, 1]', 'dummy_fact', 0, 1)) 23 | 24 | # Run the program for two timesteps to see the diffusion take place 25 | interpretation = pr.reason(timesteps=1) 26 | # pr.save_rule_trace(interpretation) 27 | 28 | # Find number of ground atoms from dictionary 29 | ga_cnt = [] 30 | d = interpretation.get_dict() 31 | for time, atoms in d.items(): 32 | ga_cnt.append(0) 33 | for comp, label_bnds in atoms.items(): 34 | ga_cnt[time] += len(label_bnds) 35 | 36 | # Make sure the computed number of ground atoms is correct 37 | assert ga_cnt == list(interpretation.get_num_ground_atoms()), 'Number of ground atoms should be the same as the computed number of ground atoms' 38 | -------------------------------------------------------------------------------- /tests/test_reason_again.py: -------------------------------------------------------------------------------- 1 | # Test if the simple hello world program works 2 | import pyreason as pr 3 | import faulthandler 4 | 5 | 6 | def test_reason_again(): 7 | # Reset PyReason 8 | pr.reset() 9 | pr.reset_rules() 10 | pr.reset_settings() 11 | 12 | # Modify the paths based on where you've stored the files we made above 13 | graph_path = './tests/friends_graph.graphml' 14 | 15 | # Modify pyreason settings to make verbose 16 | pr.settings.verbose = True # Print info to screen 17 | pr.settings.atom_trace = True # Save atom trace 18 | # pr.settings.optimize_rules = False # Disable rule optimization for debugging 19 | 20 | # Load all the files into pyreason 21 | pr.load_graphml(graph_path) 22 | pr.add_rule(pr.Rule('popular(x) <-1 popular(y), Friends(x,y), owns(y,z), owns(x,z)', 'popular_rule')) 23 | pr.add_fact(pr.Fact('popular(Mary)', 'popular_fact', 0, 1)) 24 | 25 | # Run the program for two timesteps to see the diffusion take place 26 | faulthandler.enable() 27 | interpretation = pr.reason(timesteps=1) 28 | 29 | # Now reason again 30 | new_fact = pr.Fact('popular(Mary)', 'popular_fact2', 2, 4) 31 | pr.add_fact(new_fact) 32 | interpretation = pr.reason(timesteps=3, again=True, restart=False) 33 | 34 | # Display the changes in the interpretation for each timestep 35 | dataframes = pr.filter_and_sort_nodes(interpretation, ['popular']) 36 | for t, df in enumerate(dataframes): 37 | print(f'TIMESTEP - {t}') 38 | print(df) 39 | print() 40 | 41 | assert len(dataframes[2]) == 1, 'At t=0 there should be one popular person' 42 | assert len(dataframes[3]) == 2, 'At t=1 there should be two popular people' 43 | assert len(dataframes[4]) == 3, 'At t=2 there should be three popular people' 44 | 45 | # Mary should be popular in all three timesteps 46 | assert 'Mary' in dataframes[2]['component'].values and dataframes[2].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=0 timesteps' 47 | assert 'Mary' in dataframes[3]['component'].values and dataframes[3].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=1 timesteps' 48 | assert 'Mary' in dataframes[4]['component'].values and dataframes[4].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=2 timesteps' 49 | 50 | # Justin should be popular in timesteps 1, 2 51 | assert 'Justin' in dataframes[3]['component'].values and dataframes[3].iloc[1].popular == [1, 1], 'Justin should have popular bounds [1,1] for t=1 timesteps' 52 | assert 'Justin' in dataframes[4]['component'].values and dataframes[4].iloc[2].popular == [1, 1], 'Justin should have popular bounds [1,1] for t=2 timesteps' 53 | 54 | # John should be popular in timestep 3 55 | assert 'John' in dataframes[4]['component'].values and dataframes[4].iloc[1].popular == [1, 1], 'John should have popular bounds [1,1] for t=2 timesteps' 56 | -------------------------------------------------------------------------------- /tests/test_reorder_clauses.py: -------------------------------------------------------------------------------- 1 | # Test if the simple hello world program works 2 | import pyreason as pr 3 | 4 | 5 | def test_reorder_clauses(): 6 | # Reset PyReason 7 | pr.reset() 8 | pr.reset_rules() 9 | pr.reset_settings() 10 | 11 | # Modify the paths based on where you've stored the files we made above 12 | graph_path = './tests/friends_graph.graphml' 13 | 14 | # Modify pyreason settings to make verbose 15 | pr.settings.verbose = True # Print info to screen 16 | pr.settings.atom_trace = True # Print atom trace 17 | 18 | # Load all the files into pyreason 19 | pr.load_graphml(graph_path) 20 | pr.add_rule(pr.Rule('popular(x) <-1 Friends(x,y), popular(y), owns(y,z), owns(x,z)', 'popular_rule')) 21 | pr.add_fact(pr.Fact('popular(Mary)', 'popular_fact', 0, 2)) 22 | 23 | # Run the program for two timesteps to see the diffusion take place 24 | interpretation = pr.reason(timesteps=2) 25 | 26 | # Display the changes in the interpretation for each timestep 27 | dataframes = pr.filter_and_sort_nodes(interpretation, ['popular']) 28 | for t, df in enumerate(dataframes): 29 | print(f'TIMESTEP - {t}') 30 | print(df) 31 | print() 32 | 33 | assert len(dataframes[0]) == 1, 'At t=0 there should be one popular person' 34 | assert len(dataframes[1]) == 2, 'At t=1 there should be two popular people' 35 | assert len(dataframes[2]) == 3, 'At t=2 there should be three popular people' 36 | 37 | # Mary should be popular in all three timesteps 38 | assert 'Mary' in dataframes[0]['component'].values and dataframes[0].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=0 timesteps' 39 | assert 'Mary' in dataframes[1]['component'].values and dataframes[1].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=1 timesteps' 40 | assert 'Mary' in dataframes[2]['component'].values and dataframes[2].iloc[0].popular == [1, 1], 'Mary should have popular bounds [1,1] for t=2 timesteps' 41 | 42 | # Justin should be popular in timesteps 1, 2 43 | assert 'Justin' in dataframes[1]['component'].values and dataframes[1].iloc[1].popular == [1, 1], 'Justin should have popular bounds [1,1] for t=1 timesteps' 44 | assert 'Justin' in dataframes[2]['component'].values and dataframes[2].iloc[2].popular == [1, 1], 'Justin should have popular bounds [1,1] for t=2 timesteps' 45 | 46 | # John should be popular in timestep 3 47 | assert 'John' in dataframes[2]['component'].values and dataframes[2].iloc[1].popular == [1, 1], 'John should have popular bounds [1,1] for t=2 timesteps' 48 | 49 | # Now look at the trace and make sure the order has gone back to the original rule 50 | # The second row, clause 1 should be the edge grounding ('Justin', 'Mary') 51 | rule_trace_node, _ = pr.get_rule_trace(interpretation) 52 | assert rule_trace_node.iloc[2]['Clause-1'][0] == ('Justin', 'Mary') 53 | -------------------------------------------------------------------------------- /tests/test_rule_filtering.py: -------------------------------------------------------------------------------- 1 | import pyreason as pr 2 | 3 | 4 | def test_rule_filtering(): 5 | # Reset PyReason 6 | pr.reset() 7 | pr.reset_rules() 8 | pr.reset_settings() 9 | 10 | # Modify the paths based on where you've stored the files we made above 11 | graph_path = './tests/friends_graph.graphml' 12 | 13 | # Modify pyreason settings to make verbose 14 | pr.settings.verbose = True # Print info to screen 15 | pr.settings.atom_trace = True # Print atom trace 16 | 17 | # Load all the files into pyreason 18 | pr.load_graphml(graph_path) 19 | pr.add_rule(pr.Rule('head1(x) <-1 pred1(x,y), pred2(y,z), pred3(z, w)', 'rule1')) # Should fire 20 | pr.add_rule(pr.Rule('head1(x) <-1 pred1(x,y), pred4(y,z), pred3(z, w)', 'rule2')) # Should fire 21 | pr.add_rule(pr.Rule('head2(x) <-1 pred1(x,y), pred2(y,z), pred3(z, w)', 'rule3')) # Should not fire 22 | 23 | # Dependency rules 24 | pr.add_rule(pr.Rule('pred1(x,y) <-1 pred2(x,y)', 'rule4')) # Should fire 25 | pr.add_rule(pr.Rule('pred2(x,y) <-1 pred3(x,y)', 'rule5')) # Should fire 26 | 27 | # Define the query 28 | query = pr.Query('head1(x)') 29 | 30 | # Filter the rules 31 | filtered_rules = pr.ruleset_filter.filter_ruleset([query], pr.get_rules()) 32 | filtered_rule_names = [r.get_rule_name() for r in filtered_rules] 33 | assert 'rule1' in filtered_rule_names, 'Rule 1 should be in the filtered rules' 34 | assert 'rule2' in filtered_rule_names, 'Rule 2 should be in the filtered rules' 35 | assert 'rule4' in filtered_rule_names, 'Rule 4 should be in the filtered rules' 36 | assert 'rule5' in filtered_rule_names, 'Rule 5 should be in the filtered rules' 37 | assert 'rule3' not in filtered_rule_names, 'Rule 3 should not be in the filtered rules' 38 | --------------------------------------------------------------------------------