├── .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 | [](https://www.python.org/downloads/release/python-390/)
4 | [](https://www.python.org/downloads/release/python-3100/)
5 | [](https://pyreason.readthedocs.io/en/latest/?badge=latest)
6 | [](https://github.com/lab-v2/pyreason/actions/workflows/python-publish.yml)
7 | [](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 |
--------------------------------------------------------------------------------