├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE.md ├── README.md ├── docs ├── alternatives.md ├── contributing.md ├── index.md ├── install.md ├── js │ ├── extra.js │ └── mathjax.js ├── jupyter.py ├── overrides │ └── main.html └── usage │ ├── automated-reasoning.md │ ├── grammars.md │ ├── kripke.md │ ├── models.py │ ├── natural-deduction.md │ ├── sequent-calculi.md │ ├── tableaux.md │ └── truth-tables.py ├── mathesis ├── _utils.py ├── deduction │ ├── hilbert │ │ ├── axioms.py │ │ ├── hilbert.py │ │ └── rules.py │ ├── natural_deduction │ │ ├── __init__.py │ │ ├── natural_deduction.py │ │ └── rules.py │ ├── sequent_calculus │ │ ├── __init__.py │ │ ├── rules.py │ │ ├── sequent.py │ │ └── sequent_tree.py │ └── tableau │ │ ├── __init__.py │ │ ├── rules.py │ │ ├── signed_rules.py │ │ └── tableau.py ├── forms.py ├── grammars.py ├── semantics │ ├── model.py │ └── truth_table │ │ ├── __init__.py │ │ ├── base.py │ │ ├── classical.py │ │ ├── fde.py │ │ ├── k3.py │ │ ├── l3.py │ │ └── lp.py ├── solvers.py ├── system │ ├── classical │ │ ├── __init__.py │ │ └── truth_table.py │ └── intuitionistic │ │ ├── __init__.py │ │ └── sequent_calculus │ │ ├── __init__.py │ │ └── rules.py └── truth_values │ ├── __init__.py │ ├── boolean.py │ └── numeric.py ├── mkdocs.yml ├── poetry.lock └── pyproject.toml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "${{ matrix.python-version }}" 23 | 24 | - name: Install Poetry 25 | run: | 26 | curl -sSL https://install.python-poetry.org | python3 - 27 | echo "export PATH=\"$HOME/.local/bin:$PATH\"" >> $GITHUB_ENV 28 | 29 | - name: Install dependencies 30 | run: poetry install 31 | 32 | - name: Build documentation 33 | run: poetry run mkdocs build 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _working/ 2 | 3 | ### Generated by gibo (https://github.com/simonwhitaker/gibo) 4 | ### https://raw.github.com/github/gitignore/ce5da10a3a43c4dd8bd9572eda17c0a37ee0eac1/Python.gitignore 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 110 | __pypackages__/ 111 | 112 | # Celery stuff 113 | celerybeat-schedule 114 | celerybeat.pid 115 | 116 | # SageMath parsed files 117 | *.sage.py 118 | 119 | # Environments 120 | .env 121 | .venv 122 | env/ 123 | venv/ 124 | ENV/ 125 | env.bak/ 126 | venv.bak/ 127 | 128 | # Spyder project settings 129 | .spyderproject 130 | .spyproject 131 | 132 | # Rope project settings 133 | .ropeproject 134 | 135 | # mkdocs documentation 136 | /site 137 | 138 | # mypy 139 | .mypy_cache/ 140 | .dmypy.json 141 | dmypy.json 142 | 143 | # Pyre type checker 144 | .pyre/ 145 | 146 | # pytype static type analyzer 147 | .pytype/ 148 | 149 | # Cython debug symbols 150 | cython_debug/ 151 | 152 | # PyCharm 153 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 154 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 155 | # and can be added to the global gitignore or merged into this file. For a more nuclear 156 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 157 | #.idea/ 158 | 159 | 160 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for MkDocs projects 2 | 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | 7 | version: 2 8 | 9 | # Set the version of Python and other tools you might need 10 | 11 | build: 12 | os: ubuntu-22.04 13 | 14 | tools: 15 | python: "3.11" 16 | 17 | mkdocs: 18 | configuration: mkdocs.yml 19 | 20 | # Optionally declare the Python requirements required to build your docs 21 | 22 | python: 23 | install: 24 | - requirements: docs/requirements.txt 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kentaro Ozeki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mathesis 2 | 3 | [![CI](https://github.com/digitalformallogic/mathesis/actions/workflows/ci.yml/badge.svg)](https://github.com/digitalformallogic/mathesis/actions/workflows/ci.yml) 4 | [![PyPI](https://img.shields.io/pypi/v/mathesis.svg)](https://pypi.org/project/mathesis/) 5 | [![Documentation Status](https://img.shields.io/github/actions/workflow/status/digitalformallogic/mathesis/pages/pages-build-deployment?branch=gh-pages&label=docs)](https://digitalformallogic.github.io/mathesis/) 6 | [![PyPI downloads](https://img.shields.io/pypi/dm/mathesis.svg)](https://pypistats.org/packages/mathesis) 7 | 8 | [Mathesis](//github.com/digitalformallogic/mathesis) is a human-friendly Python library for computational formal logic (including mathematical, symbolic, philosophical logic), formal semantics, and theorem proving. 9 | It is particularly well-suited for: 10 | 11 | - Students learning logic and educators teaching it 12 | - Researchers in fields like logic, philosophy, linguistics, computer science, and many others 13 | 14 | **Documentation:** 15 | 16 | ## Installation 17 | 18 | ```bash 19 | pip install mathesis 20 | ``` 21 | 22 | ## Key features 23 | 24 | - Interactive theorem proving for humans (proof assistant) 25 | - Automated reasoning (theorem prover) 26 | - Define models and check validity of inferences in the models 27 | - JupyterLab/Jupyter Notebook support 28 | - Output formulas/proofs in LaTeX 29 | - Customizable ASCII/Unicode syntax (like `A -> B`, `A → B`, `A ⊃ B` for the conditional) 30 | 31 | ## Supported logics 32 | 33 | ### Propositional logics 34 | 35 | | | Truth Table | Tableau | Natural Deduction | Sequent Calculus | 36 | |---:|:---:|:---:|:---:|:---:| 37 | | Classical logic | ✅ | ✅ | ✅ | ✅ | 38 | | Many-valued logics | ✅ | - | - | - | 39 | | Intuitionistic logic | n/a | - | - | ✅ | 40 | 41 | #### In Progress 42 | 43 | - Modal logics 44 | - Fuzzy logics 45 | - Substructural logics 46 | - Epistemic, doxastic, deontic logics 47 | - Temporal logics 48 | 49 | ### First-order logics (quantified, predicate logics) 50 | 51 | | | Model | Tableau | Natural Deduction | Sequent Calculus | 52 | |---:|:---:|:---:|:---:|:---:| 53 | | Classical logic | ✅ | ✅ | - | - | 54 | 55 | #### In Progress 56 | 57 | - Many-valued logics 58 | - Modal logics 59 | - Intuitionistic logic 60 | - Fuzzy logics 61 | - Substructural logics 62 | - Higher-order logics 63 | 64 | ## Development status 65 | 66 | ### Proof theories 67 | 68 | - **Tableaux** (semantic tableaux, analytic tableaux) 69 | * [x] Unsigned tableaux 70 | * [x] Signed tableaux 71 | - **Hilbert systems** 72 | * [ ] Hilbert systems 73 | - **Natural deduction** 74 | * [x] Generic natural deduction 75 | * [x] Gentzen-style natural deduction (Output) 76 | * [ ] Fitch-style natural deduction 77 | - **Sequent calculi** (Gentzen-style sequent calculi) 78 | - [x] Two-sided sequent calculi 79 | - [ ] Hilbert systems in sequent calculus 80 | - [ ] Natural deduction in sequent calculus 81 | 82 | ### Semantics 83 | 84 | - [x] Truth tables 85 | - [x] Set-theoretic models 86 | - [ ] Possible world semantics (Kripke semantics) 87 | - [ ] Algebraic semantics 88 | - [ ] Game-theoretic semantics 89 | - [ ] Category-theoretic semantics 90 | 91 | ## Internals 92 | 93 | - Parsing with [lark](https://github.com/lark-parser/lark) 94 | - Trees with [anytree](https://github.com/c0fec0de/anytree) 95 | 96 | ## Roadmap 97 | 98 | - [ ] Add tests 99 | - [ ] Hilbert systems 100 | - [x] Natural deduction 101 | - [ ] Boolean algebra 102 | - [ ] Type theory 103 | - [ ] Metatheorems 104 | - [ ] Output graphical representations of models 105 | - [ ] Support tptp syntax 106 | -------------------------------------------------------------------------------- /docs/alternatives.md: -------------------------------------------------------------------------------- 1 | # Alternatives 2 | 3 | ## NLTK 4 | 5 | [NLTK](https://www.nltk.org/) is a Python library for Natural Language Processing, but it includes some logic-related modules: 6 | 7 | - [NLTK :: Sample usage for logic](https://www.nltk.org/howto/logic.html) 8 | - [NLTK :: Sample usage for inference](https://www.nltk.org/howto/inference.html) 9 | - [NLTK :: Sample usage for resolution](https://www.nltk.org/howto/resolution.html) 10 | - [NLTK :: Sample usage for nonmonotonic](https://www.nltk.org/howto/nonmonotonic.html) 11 | 12 | ### Pros 13 | 14 | - A mature library with a large community 15 | - Advanced features 16 | 17 | ### Cons 18 | 19 | - Less flexible syntax 20 | - Fewer proof methods supported 21 | - No support for LaTeX output 22 | 23 | ## Others (to be added) 24 | 25 | - [PyLogics](https://whitemech.github.io/pylogics/) 26 | - [Logic - SymPy](https://docs.sympy.org/latest/modules/logic.html) 27 | - [tt: logical tools for logic](https://tt.brianwel.ch/en/latest/) 28 | - [FLiP](https://github.com/jon-jacky/FLiP) 29 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome. 4 | Open an issue or submit a pull request on GitHub digitalformallogic/mathesis. -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Mathesis: Formal Logic Library in Python" 3 | --- 4 | 5 | # Mathesis: Formal Logic Library in Python 6 | 7 | [![PyPI](https://img.shields.io/pypi/v/mathesis.svg)](https://pypi.org/project/mathesis/) 8 | [![Documentation Status](https://img.shields.io/github/actions/workflow/status/digitalformallogic/mathesis/pages/pages-build-deployment?branch=gh-pages&label=docs)](https://digitalformallogic.github.io/mathesis/) 9 | [![PyPI downloads](https://img.shields.io/pypi/dm/mathesis.svg)](https://pypistats.org/packages/mathesis) 10 | 11 | [Mathesis](//github.com/digitalformallogic/mathesis) is a human-friendly Python library for computational formal logic (including mathematical, symbolic, philosophical logic), formal semantics, and theorem proving. 12 | It is particularly well-suited for: 13 | 14 | - Students learning logic and educators teaching it 15 | - Researchers in fields like logic, philosophy, linguistics, computer science, and many others 16 | 17 | **Documentation:** 18 | 19 | ## Installation 20 | 21 | ```bash 22 | pip install mathesis 23 | ``` 24 | 25 | ## Key features 26 | 27 | - Interactive theorem proving for humans (proof assistant) 28 | - Automated reasoning (theorem prover) 29 | - Define models and check validity of inferences in the models 30 | - JupyterLab/Jupyter Notebook support 31 | - Output formulas/proofs in LaTeX 32 | - Customizable ASCII/Unicode syntax (like `A -> B`, `A → B`, `A ⊃ B` for the conditional) 33 | 34 | ## Supported logics 35 | 36 | ### Propositional logics 37 | 38 | | | Truth Table | Tableau | Natural Deduction | Sequent Calculus | 39 | |---:|:---:|:---:|:---:|:---:| 40 | | Classical logic | ✅ | ✅ | ✅ | ✅ | 41 | | Many-valued logics | ✅ | - | - | - | 42 | | Intuitionistic logic | n/a | - | - | ✅ | 43 | 44 | #### In Progress 45 | 46 | - Modal logics 47 | - Fuzzy logics 48 | - Substructural logics 49 | - Epistemic, doxastic, deontic logics 50 | - Temporal logics 51 | 52 | ### First-order logics (quantified, predicate logics) 53 | 54 | | | Model | Tableau | Natural Deduction | Sequent Calculus | 55 | |---:|:---:|:---:|:---:|:---:| 56 | | Classical logic | ✅ | ✅ | - | - | 57 | 58 | #### In Progress 59 | 60 | - Many-valued logics 61 | - Modal logics 62 | - Intuitionistic logic 63 | - Fuzzy logics 64 | - Substructural logics 65 | - Higher-order logics 66 | 67 | ## Development status 68 | 69 | ### Proof theories 70 | 71 | - **Tableaux** (semantic tableaux, analytic tableaux) 72 | * [x] Unsigned tableaux 73 | * [x] Signed tableaux 74 | - **Hilbert systems** 75 | * [ ] Hilbert systems 76 | - **Natural deduction** 77 | * [x] Generic natural deduction 78 | * [x] Gentzen-style natural deduction (Output) 79 | * [ ] Fitch-style natural deduction 80 | - **Sequent calculi** (Gentzen-style sequent calculi) 81 | - [x] Two-sided sequent calculi 82 | - [ ] Hilbert systems in sequent calculus 83 | - [ ] Natural deduction in sequent calculus 84 | 85 | ### Semantics 86 | 87 | - [x] Truth tables 88 | - [x] Set-theoretic models 89 | - [ ] Possible world semantics (Kripke semantics) 90 | - [ ] Algebraic semantics 91 | - [ ] Game-theoretic semantics 92 | - [ ] Category-theoretic semantics 93 | 94 | 98 | 99 | ## License 100 | 101 | MIT License 102 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Mathesis can be installed from PyPI with pip: 4 | 5 | ```bash 6 | pip install mathesis 7 | ``` 8 | 9 | To upgrade, add the `-U` flag: 10 | 11 | ```bash 12 | pip install -U mathesis 13 | ``` 14 | 15 | Next steps: 16 | 17 | - To use Mathesis with JupyterLab/Jupyter Notebook, see [Use with JupyterLab/Jupyter Notebook](jupyter.py) -------------------------------------------------------------------------------- /docs/js/extra.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalFormalLogic/mathesis/1fd69b1ed1cb5dc0f2c98ce9bdbc913a1a31a9dc/docs/js/extra.js -------------------------------------------------------------------------------- /docs/js/mathjax.js: -------------------------------------------------------------------------------- 1 | window.MathJax = { 2 | tex: { 3 | inlineMath: [["\\(", "\\)"]], 4 | displayMath: [["\\[", "\\]"]], 5 | processEscapes: true, 6 | processEnvironments: true, 7 | packages: { "[+]": ["bussproofs"] }, 8 | }, 9 | loader: { load: ["[tex]/bussproofs"] }, 10 | options: { 11 | ignoreHtmlClass: ".*|", 12 | processHtmlClass: "arithmatex", 13 | }, 14 | }; 15 | 16 | document$.subscribe(() => { 17 | MathJax.typesetPromise(); 18 | }); 19 | -------------------------------------------------------------------------------- /docs/jupyter.py: -------------------------------------------------------------------------------- 1 | # %% [markdown] 2 | # 3 | # Mathesis works well with JupyterLab and Jupyter notebooks. This is an example of a notebook that uses Mathesis. 4 | 5 | # %% 6 | from IPython.display import display, Math 7 | # %% 8 | from mathesis.deduction.sequent_calculus import SequentTree, rules 9 | from mathesis.grammars import BasicGrammar 10 | 11 | grammar = BasicGrammar() 12 | premises = grammar.parse(["¬A", "A∨B"]) 13 | conclusions = grammar.parse(["B"]) 14 | 15 | # %% 16 | 17 | st = SequentTree(premises, conclusions) 18 | 19 | Math(st[1].sequent.latex()) 20 | 21 | # %% 22 | st.apply(st[1], rules.Negation.Left()) 23 | print(st.tree()) 24 | 25 | # %% 26 | st.apply(st[5], rules.Disjunction.Left()) 27 | print(st.tree()) 28 | 29 | # %% 30 | st.apply(st[9], rules.Weakening.Right()) 31 | print(st.tree()) 32 | 33 | # %% 34 | st.apply(st[11], rules.Weakening.Right()) 35 | print(st.tree()) 36 | 37 | # %% 38 | print(st.latex()) 39 | -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extrahead %} 4 | 5 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /docs/usage/automated-reasoning.md: -------------------------------------------------------------------------------- 1 | # Automated Reasoning 2 | 3 | Mathesis provides simple solvers (reasoners, provers) based on truth table method and on tableau method. 4 | 5 | ## Reasoning in propositional logic 6 | 7 | ### Solvers based on truth table method 8 | 9 | See [Truth tables](truth-tables.py). 10 | 11 | ### Solvers based on tableau method 12 | 13 | `mathesis.solvers.ClassicalSolver` is a solver for classical propositional logic based on tableau method. 14 | 15 | ```python exec="1" result="text" source="material-block" 16 | from mathesis.grammars import BasicGrammar 17 | from mathesis.solvers import ClassicalSolver 18 | 19 | grammar = BasicGrammar() 20 | 21 | fml = grammar.parse("((A → B)∧(A → C)) → (A → (B∧C))") 22 | sol = ClassicalSolver().solve([], [fml]) 23 | 24 | print(sol.htree()) 25 | print(f"Valid: {sol.is_valid()}") 26 | ``` -------------------------------------------------------------------------------- /docs/usage/grammars.md: -------------------------------------------------------------------------------- 1 | # Formulas and Grammars 2 | 3 | ## Parsing formulas 4 | 5 | In Mathesis, formulas are represented as objects. 6 | Formulas are parsed from strings using grammars (languages, syntax). 7 | `mathesis.grammars.BasicGrammar` is a basic grammar with a standard set of symbols for propositional and quantified logic: 8 | 9 | - `¬` for negation, `∧` for conjunction, `∨` for disjunction, `→` for conditional. 10 | - `⊤` for top (True) and `⊥` for bottom (False). 11 | - `∀` for universal quantifier and `∃` for existential quantifier. 12 | - Arbitrary symbols are allowed for atomic formulas. 13 | 14 | For example, `¬(A→C)` is parsed as a negation of a conditional of two atomic formulas `A` and `C`. 15 | 16 | ```python exec="1" result="text" source="material-block" 17 | from mathesis.grammars import BasicGrammar 18 | 19 | grammar = BasicGrammar() 20 | 21 | fml = grammar.parse("¬(A→C)") 22 | 23 | print(fml, repr(fml)) 24 | ``` 25 | 26 | The `symbols` option allows you to customize some symbols used in the grammar. 27 | 28 | ```python exec="1" result="text" source="material-block" 29 | from mathesis.grammars import BasicGrammar 30 | 31 | grammar = BasicGrammar(symbols={"conditional": "⊃"}) 32 | 33 | fml = grammar.parse("¬(A⊃C)") 34 | 35 | print(fml, repr(fml)) 36 | ``` 37 | 38 | It accepts a list of formulas as well. 39 | 40 | ```python exec="1" result="text" source="material-block" 41 | from mathesis.grammars import BasicGrammar 42 | 43 | grammar = BasicGrammar() 44 | 45 | fmls = grammar.parse(["¬(A→C)", "B∨¬B", "(((A∧B)))"]) 46 | 47 | print([str(fml) for fml in fmls], repr(fmls)) 48 | ``` 49 | 50 | ## Advanced 51 | 52 | ### Constructing formula objects directly 53 | 54 | `mathesis.forms.Formula` is the base class for all formulas. 55 | 56 | ```python exec="1" result="text" source="material-block" 57 | from mathesis.forms import Negation, Conjunction, Disjunction, Conditional, Atom 58 | 59 | fml = Negation(Conditional(Atom("A"), Atom("C"))) 60 | 61 | print(fml, repr(fml)) 62 | ``` 63 | 64 | New connectives can be defined by subclassing `Formula`. 65 | 66 | ### Custom grammars 67 | 68 | While there is no restriction in the way that a formula string is translated into a formula object, by default Mathesis uses `lark` for parsing. 69 | Using lark, you can define arbitrary grammars in EBNF (Extended Backus-Naur Form) notation. 70 | For example, here is a simple grammar for first-order classical logic: 71 | 72 | ```python 73 | from mathesis.grammars import Grammar 74 | 75 | class MyGrammar(Grammar): 76 | grammar_rules = r""" 77 | ?fml: conditional 78 | | disjunction 79 | | conjunction 80 | | negation 81 | | universal 82 | | particular 83 | | top 84 | | bottom 85 | | atom 86 | | "(" fml ")" 87 | 88 | PREDICATE: /\w+/ 89 | TERM: /\w+/ 90 | 91 | atom : PREDICATE ("(" TERM ("," TERM)* ")")? 92 | top : "⊤" 93 | bottom : "⊥" 94 | negation : "¬" fml 95 | conjunction : fml "∧" fml 96 | disjunction : fml "∨" fml 97 | conditional : fml "→" fml 98 | universal : "∀" TERM fml 99 | particular : "∃" TERM fml 100 | 101 | %import common.WS 102 | %ignore WS 103 | """.lstrip() 104 | ``` 105 | -------------------------------------------------------------------------------- /docs/usage/kripke.md: -------------------------------------------------------------------------------- 1 | # Possible world (Kripke) semantics 2 | 3 | WIP 4 | 5 | 8 | -------------------------------------------------------------------------------- /docs/usage/models.py: -------------------------------------------------------------------------------- 1 | # %% [markdown] 2 | # ## Models 3 | # 4 | # Models can be defined using `Model` class in `mathesis.semantics.model`. The class takes four arguments: 5 | # * `domain`: domain of objects of the model 6 | # * `constants`: a dictionary mapping constant symbols to objects 7 | # * `predicates`: a dictionary mapping predicate symbols to their extensions 8 | # * `functions` (Optional): a dictionary mapping function symbols to functions over the domain 9 | 10 | # %% 11 | from mathesis.grammars import BasicGrammar 12 | from mathesis.semantics.model import Model 13 | 14 | grammar = BasicGrammar(symbols={"conditional": "→"}) 15 | 16 | model = Model( 17 | domain={"a", "b", "c"}, 18 | constants={ 19 | "a": "a", 20 | "b": "b", 21 | }, 22 | predicates={ 23 | "P": {"a", "b"}, 24 | "Q": {"a"}, 25 | "R": {("a", "b"), ("c", "a")}, 26 | }, 27 | ) 28 | 29 | # %% [markdown] 30 | # `model.valuate()` takes a formula and a variable assignment and returns the truth value of the formula in the model. 31 | 32 | # %% 33 | fml = grammar.parse("P(a) → R(x, b)") 34 | model.valuate(fml, variable_assignment={"x": "c"}) 35 | 36 | # %% 37 | model.valuate(fml, variable_assignment={"x": "a"}) 38 | 39 | # %% [markdown] 40 | # `model.validates()` takes premises and conclusions and returns whether the model validates the inference. 41 | 42 | # %% 43 | fml = grammar.parse("∀x(∃y((Q(x)∨Q(b))→R(x, y)))") 44 | model.validates(premises=[], conclusions=[fml]) 45 | 46 | # %% [markdown] 47 | # ## Countermodel construction 48 | 49 | # WIP 50 | -------------------------------------------------------------------------------- /docs/usage/natural-deduction.md: -------------------------------------------------------------------------------- 1 | # Proof in Natural Deduction 2 | 3 | ## Introduction 4 | 5 | In Mathesis, a _state_ of a natural deduction proof (and its subproofs) consists of the premises and the conclusion that are available to the (sub)proof at a given step. 6 | A state is displayed as ``. 7 | 8 | Intuitively, the formulas on the left side of `⇒` are what to come to the upper part of the final (sub)proof, and those on the right side of `⇒` are what to come to the lower part of the final (sub)proof. 9 | 10 | Natural deduction is a proof system that consists of elimination rules and introduction rules. In Mathesis, 11 | 12 | - you can apply an **elimination rule** *up-to-down* to the premises of a (sub)proof to obtain new premises. 13 | - Similarly, you can apply an **introduction rule** *down-to-up* to the conclusion of a (sub)proof and convert it into new subproofs. 14 | 15 | ```python exec="1" result="text" source="material-block" 16 | from mathesis.grammars import BasicGrammar 17 | from mathesis.deduction.natural_deduction import NDTree, rules 18 | 19 | grammar = BasicGrammar() 20 | 21 | premises = grammar.parse(["A∨B", "B→C"]) 22 | conclusion = grammar.parse("A∨C") 23 | deriv = NDTree(premises, conclusion) 24 | print(deriv.tree()) 25 | 26 | deriv.apply(deriv[1], rules.Disjunction.Elim()) 27 | print(deriv.tree()) 28 | 29 | deriv.apply(deriv[7], rules.Disjunction.Intro("left")) 30 | print(deriv.tree()) 31 | 32 | deriv.apply(deriv[10], rules.Conditional.Elim()) 33 | print(deriv.tree()) 34 | 35 | deriv.apply(deriv[25], rules.Disjunction.Intro("right")) 36 | print(deriv.tree()) 37 | ``` 38 | 39 | ## Render as Gentzen-style Proof 40 | 41 | (WIP) Mathesis has an experimental support for rendering a natural deduction proof as a Gentzen-style proof or its LaTeX code. 42 | 43 | ```python exec="1" result="text" source="material-block" 44 | from mathesis.grammars import BasicGrammar 45 | from mathesis.deduction.natural_deduction import NDTree, rules 46 | 47 | grammar = BasicGrammar() 48 | 49 | premises = grammar.parse(["A∨B", "B→C"]) 50 | conclusion = grammar.parse("A∨C") 51 | deriv = NDTree(premises, conclusion) 52 | print(deriv.tree(number=False)) 53 | 54 | deriv.apply(deriv[1], rules.Disjunction.Elim()) 55 | deriv.apply(deriv[7], rules.Disjunction.Intro("left")) 56 | deriv.apply(deriv[10], rules.Conditional.Elim()) 57 | deriv.apply(deriv[25], rules.Disjunction.Intro("right")) 58 | 59 | print(deriv.tree(style="gentzen")) 60 | 61 | print(deriv.latex()) 62 | ``` 63 | 64 | $$ 65 | \begin{prooftree} 66 | \AxiomC{$A$} 67 | \RightLabel{$\lor$I} 68 | \UnaryInfC{$A \lor C$} 69 | \AxiomC{$B$} 70 | \RightLabel{$\to$E} 71 | \AxiomC{$B \to C$} 72 | \BinaryInfC{$C$} 73 | \RightLabel{$\lor$I} 74 | \UnaryInfC{$A \lor C$} 75 | \RightLabel{$\lor$E} 76 | \AxiomC{$A \lor B$} 77 | \TrinaryInfC{$A \lor C$} 78 | \end{prooftree} 79 | $$ 80 | 81 | ## Render as Fitch-style Proof 82 | 83 | WIP 84 | 85 | ## Render as Suppes-Lemmon-style Proof 86 | 87 | WIP 88 | 89 | ## Render as Sequent Calculus Proof 90 | 91 | WIP 92 | -------------------------------------------------------------------------------- /docs/usage/sequent-calculi.md: -------------------------------------------------------------------------------- 1 | # Proof in Sequent Calculus 2 | 3 | Sequent calculus (plural: calculi) is a formal proof system based on *sequents*, which normally are expressions of the form $\Gamma \vdash \Delta$, where $\Gamma$ and $\Delta$ are lists or sets of formulas. 4 | 5 | ## Sequent trees and applications of rules 6 | 7 | `mathesis.deduction.sequent_calculus.SequentTree` is a class for sequent trees (proof trees, proof diagrams). 8 | It is initialized with an inference, given as a list of premises and of conclusions. 9 | Rules are applied to a sequent in a sequent tree with `st.apply(node, rule)` where `st` is a sequent tree. 10 | 11 | ```python exec="1" result="text" source="material-block" 12 | from mathesis.deduction.sequent_calculus import SequentTree, rules 13 | from mathesis.grammars import BasicGrammar 14 | 15 | grammar = BasicGrammar() 16 | premises = grammar.parse(["¬A", "A∨B"]) 17 | conclusions = grammar.parse(["B"]) 18 | 19 | st = SequentTree(premises, conclusions) 20 | 21 | print(st.tree()) 22 | st.apply(st[1], rules.Negation.Left()) 23 | print(st.tree()) 24 | st.apply(st[5], rules.Disjunction.Left()) 25 | print(st.tree()) 26 | st.apply(st[9], rules.Weakening.Right()) 27 | print(st.tree()) 28 | st.apply(st[12], rules.Weakening.Right()) 29 | print(st.tree()) 30 | ``` 31 | 32 | ## Render the proof in LaTeX 33 | 34 | A sequent tree object can be rendered in LaTeX with `st.latex()`. 35 | The LaTeX output uses a `prooftree` environment of `bussproofs` package. 36 | MathJax supports `bussproofs` package, so you can render the LaTeX output on a Web page. 37 | 38 | ```python exec="1" result="text" source="material-block" 39 | from mathesis.deduction.sequent_calculus import SequentTree, rules 40 | from mathesis.grammars import BasicGrammar 41 | 42 | grammar = BasicGrammar() 43 | premises = grammar.parse(["¬A", "A∨B"]) 44 | conclusions = grammar.parse(["B"]) 45 | 46 | st = SequentTree(premises, conclusions) 47 | 48 | print(st.tree()) 49 | print(st.latex(number=False), "\n") 50 | 51 | st.apply(st[1], rules.Negation.Left()) 52 | # print(st.tree()) 53 | st.apply(st[5], rules.Disjunction.Left()) 54 | # print(st.tree()) 55 | st.apply(st[9], rules.Weakening.Right()) 56 | # print(st.tree()) 57 | st.apply(st[12], rules.Weakening.Right()) 58 | print(st.tree()) 59 | print(st.latex(number=False)) 60 | ``` 61 | 62 | $$ 63 | \begin{prooftree} 64 | \AxiomC{$\neg A, A \lor B \Rightarrow B$} 65 | \end{prooftree} 66 | $$ 67 | 68 | $$ 69 | \begin{prooftree} 70 | \AxiomC{$A \Rightarrow A$} 71 | \RightLabel{wR} 72 | \UnaryInfC{$A \Rightarrow A, B$} 73 | \AxiomC{$B \Rightarrow A$} 74 | \RightLabel{wR} 75 | \UnaryInfC{$B \Rightarrow A, B$} 76 | \RightLabel{$\lor$L} 77 | \BinaryInfC{$A \lor B \Rightarrow A, B$} 78 | \RightLabel{$\neg$L} 79 | \UnaryInfC{$\neg A, A \lor B \Rightarrow B$} 80 | \end{prooftree} 81 | $$ -------------------------------------------------------------------------------- /docs/usage/tableaux.md: -------------------------------------------------------------------------------- 1 | # Proof in Tableau 2 | 3 | Semantic tableau (plural: tableaux) is a decision/proof procedure for propositional and quantified logics. 4 | 5 | ## Unsigned tableaux 6 | 7 | `mathesis.deduction.tableau.Tableau` is a class for unsigned tableaux. 8 | It is initialized with an inference, given as a list of premises and of conclusions. 9 | 10 | ```python exec="1" result="text" source="material-block" 11 | from mathesis.grammars import BasicGrammar 12 | from mathesis.deduction.tableau import Tableau 13 | 14 | grammar = BasicGrammar() 15 | 16 | premises = grammar.parse(["A→B", "B→C"]) 17 | conclusions = grammar.parse(["A→C"]) 18 | tab = Tableau(premises, conclusions) 19 | 20 | print(tab.htree()) 21 | ``` 22 | 23 | A tableau is a tree of nodes, each node being a formula. 24 | Mathesis automatically indexes the nodes, so that you can access them by their index. 25 | You can apply a rule to a node of tableau with `tab.apply(node, rule)` where `tab` is a tableau: 26 | 27 | ```python exec="1" result="text" source="material-block" 28 | from mathesis.grammars import BasicGrammar 29 | from mathesis.deduction.tableau import Tableau, rules 30 | 31 | grammar = BasicGrammar() 32 | 33 | premises = grammar.parse(["A→B", "B→C"]) 34 | conclusions = grammar.parse(["A→C"]) 35 | tab = Tableau(premises, conclusions) 36 | 37 | print(tab.htree()) 38 | print(f"Closed: {tab.is_closed()}\n") 39 | 40 | tab.apply(tab[3], rules.NegatedConditionalRule()) 41 | print(tab.htree()) 42 | tab.apply(tab[1], rules.ConditionalRule()) 43 | print(tab.htree()) 44 | tab.apply(tab[2], rules.ConditionalRule()) 45 | print(tab.htree()) 46 | 47 | print(f"Closed: {tab.is_closed()}") 48 | ``` 49 | 50 | A branch is a path from the root to a leaf of the tableau. 51 | A branch is closed if it contains a contradiction (i.e., contradictory formulas.) 52 | The tableau is closed if all branches are closed. 53 | 54 | ## Signed tableaux 55 | 56 | A signed tableau is a tableau where each node is signed with a truth value. 57 | 58 | ```python exec="1" result="text" source="material-block" 59 | from mathesis.grammars import BasicGrammar 60 | from mathesis.deduction.tableau import SignedTableau, signed_rules 61 | 62 | grammar = BasicGrammar() 63 | 64 | premises = grammar.parse(["A→B", "B→C"]) 65 | conclusions = grammar.parse(["A→C"]) 66 | tab = SignedTableau(premises, conclusions) 67 | 68 | print(tab.htree()) 69 | ``` 70 | 71 | ## First-order logic 72 | 73 | In first-order logic, the rules extend to quantifiers as follows: 74 | 75 | ### Unsigned tableaux 76 | 77 | - `NegatedParticularRule` 78 | - `NegatedUniversalRule` 79 | - `UniversalInstantiationRule` 80 | - `ParticularInstantiationRule` 81 | 82 | ```python exec="1" result="text" source="material-block" 83 | from mathesis.grammars import BasicGrammar 84 | from mathesis.deduction.tableau import Tableau, rules 85 | 86 | grammar = BasicGrammar() 87 | 88 | premises = grammar.parse(["P(a)", "∀x(P(x)→Q(x))"]) 89 | conclusions = grammar.parse(["Q(a)"]) 90 | tab = Tableau(premises, conclusions) 91 | 92 | print(tab.htree()) 93 | print(f"Closed: {tab.is_closed()}\n") 94 | 95 | tab.apply(tab[2], rules.UniversalInstantiationRule(replacing_term="a")) 96 | print(tab.htree()) 97 | 98 | tab.apply(tab[4], rules.ConditionalRule()) 99 | print(tab.htree()) 100 | 101 | print(f"Closed: {tab.is_closed()}\n") 102 | ``` 103 | 104 | ### Signed tableaux 105 | 106 | WIP 107 | 108 | ## Further reading 109 | 110 | See [Automated reasoning](automated-reasoning.md) for automated reasoning with tableaux. 111 | -------------------------------------------------------------------------------- /docs/usage/truth-tables.py: -------------------------------------------------------------------------------- 1 | # %% [markdown] 2 | # # Truth tables 3 | 4 | # ## Show truth tables for connectives 5 | 6 | # Truth functions are defined as _clauses_ in mathesis. 7 | # You can show the truth tables for the clauses in HTML in JupyerLab/Jupyter Notebook: 8 | 9 | # %% 10 | from mathesis.system.classical.truth_table import ConditionalClause 11 | 12 | conditional_clause = ConditionalClause() 13 | conditional_clause 14 | 15 | # %% [markdown] 16 | # Outside Jupyer, you get a plain text table: 17 | # %% 18 | print(conditional_clause) 19 | 20 | # %% [markdown] 21 | # ## Generate truth tables for classical logic 22 | 23 | # `mathesis.semantics.truth_table.ClassicalTruthTable` automatically generates the truth table for a given formula. 24 | 25 | # %% 26 | from mathesis.grammars import BasicGrammar 27 | from mathesis.semantics.truth_table import ClassicalTruthTable 28 | 29 | grammar = BasicGrammar() 30 | 31 | fml = grammar.parse("(¬P∧(P∨Q))→Q") 32 | 33 | table = ClassicalTruthTable(fml) 34 | table 35 | 36 | # %% [markdown] 37 | # `table.is_valid()` just returns whether the formula is valid. 38 | 39 | # %% 40 | f"Valid: {table.is_valid()}" 41 | 42 | # %% [markdown] 43 | # ## Generate truth tables for many-valued logics 44 | 45 | # Some many-valued logics are implemented out of the box. They are available from `mathesis.semantics.truth_table`. 46 | 47 | # %% [markdown] 48 | # ### Three-valued logic K3 and Ł3 49 | 50 | # #### Kleene's K3 51 | 52 | # %% 53 | from mathesis.grammars import BasicGrammar 54 | from mathesis.semantics.truth_table import K3TruthTable 55 | 56 | grammar = BasicGrammar() 57 | 58 | fml = grammar.parse("A∨¬A") 59 | 60 | table = K3TruthTable(fml) 61 | table 62 | 63 | # %% 64 | f"Valid: {table.is_valid()}" 65 | 66 | # %% [markdown] 67 | # #### Łukasiewicz's Ł3 68 | 69 | # %% 70 | from mathesis.grammars import BasicGrammar 71 | from mathesis.semantics.truth_table import L3TruthTable 72 | 73 | grammar = BasicGrammar() 74 | 75 | fml = grammar.parse("A→A") 76 | 77 | table = L3TruthTable(fml) 78 | table 79 | # %% [markdown] 80 | # ### Three-valued logic LP 81 | 82 | # %% 83 | from mathesis.grammars import BasicGrammar 84 | from mathesis.semantics.truth_table import LPTruthTable 85 | 86 | grammar = BasicGrammar() 87 | 88 | fml = grammar.parse("(A∧¬A)→A") 89 | 90 | table = LPTruthTable(fml) 91 | table 92 | 93 | # %% 94 | f"Valid: {table.is_valid()}" 95 | 96 | # %% [markdown] 97 | # ### Four-valued logic FDE 98 | 99 | # %% 100 | from mathesis.grammars import BasicGrammar 101 | from mathesis.semantics.truth_table import FDETruthTable 102 | 103 | grammar = BasicGrammar() 104 | 105 | fml = grammar.parse("(A∧¬A)→A") 106 | 107 | table = FDETruthTable(fml) 108 | table 109 | # %% [markdown] 110 | # ## Use custom symbols for truth values 111 | 112 | # Subclasses of `ConnectiveClause()` and `TruthTable()` can have `truth_value_symbols` attribute that is a dictionary mapping internal numeric truth values to arbitrary symbols like ⊤, ⊥, T, F, etc. 113 | 114 | # %% [markdown] 115 | # ## Define custom truth tables 116 | 117 | # WIP 118 | -------------------------------------------------------------------------------- /mathesis/_utils.py: -------------------------------------------------------------------------------- 1 | def flatten_list(l): 2 | for el in l: 3 | if isinstance(el, list): 4 | yield from flatten_list(el) 5 | else: 6 | yield el 7 | -------------------------------------------------------------------------------- /mathesis/deduction/hilbert/axioms.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from itertools import count 3 | 4 | from anytree import Node 5 | 6 | from mathesis.forms import Conditional, Conjunction, Disjunction, Formula, Negation 7 | from mathesis.deduction.sequent_calculus.rules import Rule, SequentItem, _apply, sign 8 | 9 | 10 | def _apply_axiom(target: Node, axiom: Formula, counter=count(1)): 11 | assert target.sign == sign.NEGATIVE, "Not on the right side of sequent" 12 | 13 | new_item = SequentItem(axiom, sign.POSITIVE, n=next(counter)) 14 | branch_sequent = _apply(target, [target.clone(), new_item], counter) 15 | 16 | return { 17 | "queue_items": [branch_sequent], 18 | "counter": counter, 19 | } 20 | 21 | 22 | class Axiom: 23 | formula: Formula 24 | 25 | 26 | class SAxiom(Axiom): 27 | """Axiom schema of substitution""" 28 | 29 | def __init__(self, p: Formula, q: Formula, r: Formula): 30 | self.formula = Conditional( 31 | Conditional(p, Conditional(q, r)), 32 | Conditional(Conditional(p, q), Conditional(p, r)), 33 | ) 34 | 35 | 36 | class KAxiom(Axiom): 37 | """Axiom schema of constant""" 38 | 39 | def __init__(self, p: Formula, q: Formula): 40 | self.formula = Conditional(p, Conditional(q, p)) 41 | 42 | 43 | class IdentityAxiom(Axiom): 44 | """Axiom schema of identity""" 45 | 46 | def __init__(self, p: Formula): 47 | self.formula = Conditional(p, p) 48 | 49 | 50 | class DisjunctionIntroductionAxiom: 51 | """Axiom schema of disjunction introduction""" 52 | 53 | def __init__(self, disj1: Formula, disj2: Formula, antecendent: Formula): 54 | """ 55 | Args: 56 | disj1 (Formula): Left disjunct 57 | disj2 (Formula): Right disjunct 58 | antecendent (Formula): Antecendent identical to one of the disjuncts 59 | """ 60 | if antecendent != disj1 and antecendent != disj2: 61 | raise ValueError("Antecendent must be one of the disjunctions") 62 | self.formula = Conditional(disj1, Disjunction(disj1, disj2)) 63 | 64 | 65 | class DisjunctionEliminationAxiom: 66 | """Axiom schema of disjunction elimination""" 67 | 68 | def __init__(self, disj1: Formula, disj2: Formula, consequent: Formula): 69 | self.formula = Conditional( 70 | Conditional(disj1, consequent), 71 | Conditional( 72 | Conditional(disj2, consequent), 73 | Conditional(Disjunction(disj1, disj2), consequent), 74 | ), 75 | ) 76 | 77 | 78 | class ConjunctionIntroductionAxiom: 79 | """Axiom schema of conjunction introduction""" 80 | 81 | def __init__(self, conj1: Formula, conj2: Formula): 82 | self.formula = Conditional(conj1, Conditional(conj2, Conjunction(conj1, conj2))) 83 | 84 | 85 | class ConjunctionEliminationAxiom: 86 | """Axiom schema of conjunction elimination""" 87 | 88 | def __init__(self, conj1: Formula, conj2: Formula, consequent: Formula): 89 | if consequent != conj1 and consequent != conj2: 90 | raise ValueError("Antecendent must be one of the disjunctions") 91 | self.formula = Conditional( 92 | Conjunction(conj1, conj2), 93 | consequent, 94 | ) 95 | 96 | 97 | class DNEAxiom: 98 | """Axiom schema of double negation elimination""" 99 | 100 | def __init__(self, p: Formula): 101 | self.formula = Conditional(Negation(Negation(p)), p) 102 | -------------------------------------------------------------------------------- /mathesis/deduction/hilbert/hilbert.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from itertools import count 3 | from operator import itemgetter 4 | from types import SimpleNamespace 5 | from typing import Any 6 | 7 | from anytree import Node, RenderTree, find_by_attr 8 | 9 | from mathesis import _utils, forms 10 | from mathesis.deduction.sequent_calculus import SequentTree 11 | from mathesis.forms import Formula 12 | from mathesis.deduction.hilbert.axioms import Axiom, _apply_axiom 13 | 14 | _logger = logging.getLogger(__name__) 15 | _logger.addHandler(logging.NullHandler()) 16 | 17 | 18 | class Hilbert: 19 | counter = count(1) 20 | 21 | def __init__(self, premises: list[Formula], conclusion: Formula): 22 | assert isinstance(conclusion, Formula), "Conclusion must be a single formula" 23 | self._sequent_tree = SequentTree(premises, [conclusion]) 24 | self.bookkeeper = self._sequent_tree.bookkeeper 25 | self.counter = self._sequent_tree.counter 26 | 27 | # Proof tree 28 | for item in self._sequent_tree.root.left: 29 | item.subproof = Node(item, children=[]) 30 | self._sequent_tree.root.right[0].subproof = Node( 31 | self._sequent_tree.root.right[0], 32 | children=[item.subproof for item in self._sequent_tree.root.left], 33 | ) 34 | 35 | def __getitem__(self, index): 36 | return self.bookkeeper[index] 37 | 38 | def axiom(self, target, axiom: Axiom): 39 | res = _apply_axiom(target, axiom.formula, self.counter) 40 | queue_items = res["queue_items"] 41 | 42 | # new_sequents = [] 43 | 44 | for branch in queue_items: 45 | for node in branch.items: 46 | self.bookkeeper[node.n] = node 47 | branch.parent = target.sequent 48 | # new_sequents.append(branch) 49 | 50 | return self 51 | 52 | def apply(self, target: Node, rule): 53 | res = rule.apply(target, self.counter) 54 | queue_items = res["queue_items"] 55 | 56 | # new_sequents = [] 57 | 58 | for branch in queue_items: 59 | for node in branch.items: 60 | self.bookkeeper[node.n] = node 61 | branch.parent = target.sequent 62 | # new_sequents.append(branch) 63 | 64 | return self 65 | 66 | def tree(self, style=None, number=True): 67 | if style == "gentzen": 68 | output = "" 69 | root = self._sequent_tree.root.right[0].subproof 70 | for pre, fill, node in RenderTree(root): 71 | output += "{}{} {}\n".format( 72 | pre, 73 | f"[{node.name}]" if getattr(node, "marked", False) else node.name, 74 | # self.proof_tree.mapping[node].n, 75 | " ×" if getattr(node, "marked", False) else "", 76 | ) 77 | return output 78 | else: 79 | return self._sequent_tree.tree(number=number) 80 | 81 | # def latex(self, number=False, arrow=r"\Rightarrow"): 82 | # output = "" 83 | # root = self._sequent_tree.root.right[0].subproof 84 | # for node in PostOrderIter(root): 85 | # tmpl = "" 86 | # if len(node.children) == 0: 87 | # tmpl = r"\AxiomC{{${}$}}" 88 | # elif len(node.children) == 1: 89 | # tmpl = r"\UnaryInfC{{${}$}}" 90 | # elif len(node.children) == 2: 91 | # tmpl = r"\BinaryInfC{{${}$}}" 92 | # elif len(node.children) == 3: 93 | # tmpl = r"\TrinaryInfC{{${}$}}" 94 | # output += tmpl.format(node.name.fml.latex()) + "\n" 95 | # return """\\begin{{prooftree}}\n{}\\end{{prooftree}}""".format(output) 96 | -------------------------------------------------------------------------------- /mathesis/deduction/hilbert/rules.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from itertools import count 3 | 4 | from anytree import Node 5 | 6 | from mathesis import forms 7 | from mathesis.deduction.sequent_calculus.rules import Rule, SequentItem, _apply, sign 8 | 9 | # from mathesis.deduction.natural_deduction.rules import _apply 10 | 11 | 12 | class ModusPonens(Rule): 13 | def apply(self, target, counter=count(1)): 14 | assert target.sign == sign.POSITIVE, "Cannot apply elimination rule" 15 | assert isinstance(target.fml, forms.Conditional), "Not a conditional" 16 | antec, conseq = target.fml.subs 17 | 18 | antec = next( 19 | filter(lambda x: x.fml == antec, target.sequent.left), 20 | None, 21 | ) 22 | assert antec, "Antecendent does not match" 23 | 24 | # conclusion = str(target.sequent.right[0].fml) 25 | # print(conclusion) 26 | # assert str(conseq) == conclusion, "Consequent does not match" 27 | 28 | conseq = SequentItem(conseq, sign=sign.POSITIVE, n=next(counter)) 29 | sequent = _apply(target, [conseq], counter) 30 | 31 | # # Subproof 32 | # conseq.subproof = Node( 33 | # conseq, 34 | # children=[ 35 | # deepcopy(antec.subproof), 36 | # deepcopy(target.subproof), 37 | # ], 38 | # parent=target.sequent.right[0].subproof, 39 | # ) 40 | # # target.sequent.right[0].subproof = sequent.right[0].subproof 41 | # sequent.right[0].subproof = target.sequent.right[0].subproof 42 | 43 | # if sequent.tautology(): 44 | # target.sequent.right[0].subproof.children = conseq.subproof.children 45 | 46 | return { 47 | "queue_items": [sequent], 48 | "counter": counter, 49 | } 50 | -------------------------------------------------------------------------------- /mathesis/deduction/natural_deduction/__init__.py: -------------------------------------------------------------------------------- 1 | from mathesis.deduction.natural_deduction.natural_deduction import NDTree # noqa 2 | -------------------------------------------------------------------------------- /mathesis/deduction/natural_deduction/natural_deduction.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from itertools import count 4 | 5 | from anytree import NodeMixin, PostOrderIter, RenderTree 6 | 7 | from mathesis.deduction import natural_deduction 8 | from mathesis.deduction.sequent_calculus import SequentTree 9 | from mathesis.deduction.sequent_calculus.sequent import SequentItem 10 | from mathesis.forms import Formula 11 | 12 | 13 | class NDSequentItem(SequentItem): 14 | subproof: NDSubproof 15 | derived_by: natural_deduction.rules.Rule | None 16 | 17 | 18 | class NDSubproof(NodeMixin): 19 | derived_by: natural_deduction.rules.Rule | None 20 | 21 | def __init__(self, item: SequentItem, *, parent=None, children=[]) -> None: 22 | super().__init__() 23 | self.name = item 24 | self.item = item 25 | self.parent = parent 26 | self.children = children 27 | 28 | self.derived_by = None 29 | 30 | 31 | class NDTree: 32 | """A natural deduction proof tree.""" 33 | 34 | _sequent_tree: SequentTree 35 | bookkeeper: dict[int, SequentItem] 36 | counter: count[int] 37 | 38 | def __init__(self, premises: list[Formula], conclusion: Formula): 39 | assert isinstance(conclusion, Formula), "Conclusion must be a single formula" 40 | self._sequent_tree = SequentTree(premises, [conclusion]) 41 | self.bookkeeper = self._sequent_tree.bookkeeper 42 | self.counter = self._sequent_tree.counter 43 | 44 | # Proof tree 45 | for item in self._sequent_tree.root.left: 46 | item.subproof = NDSubproof(item, children=[]) 47 | self._sequent_tree.root.right[0].subproof = NDSubproof( 48 | self._sequent_tree.root.right[0], 49 | children=[item.subproof for item in self._sequent_tree.root.left], 50 | ) 51 | 52 | def __getitem__(self, index): 53 | return self.bookkeeper[index] 54 | 55 | def apply(self, target: SequentItem, rule): 56 | res = rule.apply(target, self.counter) 57 | queue_items = res["queue_items"] 58 | 59 | # new_sequents = [] 60 | 61 | for branch in queue_items: 62 | for node in branch.items: 63 | self.bookkeeper[node.n] = node 64 | branch.parent = target.sequent 65 | # new_sequents.append(branch) 66 | 67 | return self 68 | 69 | def tree(self, style=None, number=True): 70 | if style == "gentzen": 71 | output = "" 72 | root = self._sequent_tree.root.right[0].subproof 73 | for pre, fill, node in RenderTree(root): 74 | output += "{}{}{}\n".format( 75 | pre, 76 | f"[{node.name}]" if getattr(node, "marked", False) else node.name, 77 | # self.proof_tree.mapping[node].n, 78 | " ×" if getattr(node, "marked", False) else "", 79 | ) 80 | return output 81 | else: 82 | return self._sequent_tree.tree(number=number) 83 | 84 | def latex(self, number=False, arrow=r"\Rightarrow"): 85 | output = "" 86 | root = self._sequent_tree.root.right[0].subproof 87 | for node in PostOrderIter(root): 88 | if node.derived_by is not None and hasattr(node.derived_by, "latex"): 89 | label_part = f"\\RightLabel{{{node.derived_by.latex()}}}\n" 90 | else: 91 | label_part = "" 92 | 93 | tmpl = "" 94 | if len(node.children) == 0: 95 | tmpl = r"\AxiomC{{${}$}}" 96 | elif len(node.children) == 1: 97 | tmpl = r"\UnaryInfC{{${}$}}" 98 | elif len(node.children) == 2: 99 | tmpl = r"\BinaryInfC{{${}$}}" 100 | elif len(node.children) == 3: 101 | tmpl = r"\TrinaryInfC{{${}$}}" 102 | output += label_part + tmpl.format(node.name.fml.latex()) + "\n" 103 | 104 | return """\\begin{{prooftree}}\n{}\\end{{prooftree}}""".format(output) 105 | 106 | def _typst(self, number=False, arrow=r"\Rightarrow"): 107 | output = "" 108 | root = self._sequent_tree.root.right[0].subproof 109 | 110 | def rec(node): 111 | if node.derived_by is not None: 112 | label_part = f"\nname: [{node.derived_by}],\n" 113 | else: 114 | label_part = "\n" if not len(node.children) == 0 else "" 115 | 116 | if len(node.children) == 0: 117 | return f"${node.name.fml}$,{label_part}" 118 | elif len(node.children) == 1: 119 | return f"rule({label_part}${node.name.fml}$,\n{rec(node.children[0])})" 120 | elif len(node.children) == 2: 121 | return f"rule({label_part}${node.name.fml}$,\n{rec(node.children[0])}\n{rec(node.children[1])})" 122 | elif len(node.children) == 3: 123 | return f"rule({label_part}${node.name.fml}$,\n{rec(node.children[0])},\n{rec(node.children[1])},\n{rec(node.children[2])})" 124 | 125 | rec_output = rec(root) 126 | 127 | output = f"#proof-tree(\n{rec_output}\n)" 128 | 129 | return output 130 | -------------------------------------------------------------------------------- /mathesis/deduction/natural_deduction/rules.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from itertools import count 3 | from typing import Literal 4 | 5 | from mathesis import forms 6 | from mathesis.deduction.natural_deduction.natural_deduction import NDSubproof 7 | from mathesis.deduction.sequent_calculus import Sequent, SequentItem 8 | from mathesis.deduction.tableau import sign 9 | 10 | 11 | def _apply(target, new_items, counter, preserve_target=True): 12 | branch_items = new_items 13 | new_target = None 14 | 15 | for item in target.sequent.items: 16 | if item != target or preserve_target: 17 | node = item.clone() 18 | node.n = next(counter) 19 | if item == target: 20 | new_target = node 21 | branch_items.append(node) 22 | 23 | branch_sequent = Sequent([], [], parent=target.sequent) 24 | branch_sequent.items = branch_items 25 | 26 | if preserve_target: 27 | return branch_sequent, target 28 | else: 29 | return branch_sequent 30 | 31 | 32 | class Rule: 33 | label: str 34 | latex_label: str 35 | 36 | def __str__(self): 37 | return self.label 38 | 39 | def latex(self): 40 | return self.latex_label 41 | 42 | 43 | class IntroductionRule(Rule): 44 | pass 45 | 46 | 47 | class EliminationRule(Rule): 48 | pass 49 | 50 | 51 | class EFQ(Rule): 52 | label = "EFQ" 53 | latex_label = "EFQ" 54 | 55 | def __init__(self, intro: SequentItem): 56 | self.intro = intro 57 | 58 | def apply(self, target: SequentItem, counter=count(1)): 59 | assert target.sign == sign.POSITIVE, "Invalid application" 60 | # TODO: Fix this 61 | assert str(target.fml) == "⊥", "Not an atom" 62 | 63 | target.sequent.derived_by = self 64 | target.subproof.derived_by = self 65 | 66 | item = SequentItem( 67 | self.intro.fml, 68 | sign=sign.POSITIVE, 69 | n=next(counter), 70 | ) 71 | sq, _target = _apply(target, [item], counter) 72 | 73 | # Subproof 74 | item.subproof = NDSubproof( 75 | item, 76 | parent=target.sequent.right[0].subproof, 77 | children=[target.subproof], 78 | ) 79 | # target.subproof = Node(target, children=[item.subproof]) 80 | # _target.sequent.right[0].subproof = target.sequent.right[0].subproof 81 | sq.right[0].subproof = target.sequent.right[0].subproof 82 | 83 | if sq.tautology(): 84 | target.sequent.right[0].subproof.children = item.subproof.children 85 | 86 | return { 87 | "queue_items": [sq], 88 | "counter": counter, 89 | } 90 | 91 | 92 | class Negation: 93 | # Intro = signed_rules.NegativeNegationRule 94 | class Intro(IntroductionRule): 95 | label = "¬I" 96 | latex_label = r"$\neg$I" 97 | 98 | def apply(self, target: SequentItem, counter=count(1)): 99 | assert target.sign == sign.NEGATIVE, "Cannot apply introduction rule" 100 | assert isinstance(target.fml, forms.Negation), "Not a negation" 101 | subfml = target.fml.sub 102 | 103 | if target.sequent: 104 | target.sequent.derived_by = self 105 | 106 | target.subproof.derived_by = self 107 | 108 | # TODO: Fix this 109 | falsum = forms.Atom("⊥", latex=r"\bot") 110 | 111 | antec = SequentItem(subfml, sign=sign.POSITIVE, n=next(counter)) 112 | conseq = SequentItem( 113 | falsum, 114 | sign=sign.NEGATIVE, 115 | n=next(counter), 116 | ) 117 | sq = _apply(target, [antec, conseq], counter, preserve_target=False) 118 | 119 | # Attach a subproof to the consequent (falsum) 120 | conseq.subproof = NDSubproof( 121 | conseq, 122 | children=[deepcopy(node.subproof) for node in target.sequent.left], 123 | ) 124 | target.sequent.right[0].subproof.children = [conseq.subproof] 125 | antec.subproof = NDSubproof( 126 | antec, 127 | parent=conseq.subproof, 128 | children=[], 129 | ) 130 | 131 | return { 132 | "queue_items": [sq], 133 | "counter": counter, 134 | } 135 | 136 | class Elim(EliminationRule): 137 | label = "¬E" 138 | latex_label = r"$\neg$E" 139 | 140 | def __init__(self): 141 | pass 142 | 143 | def apply(self, target: SequentItem, counter=count(1)): 144 | assert target.sign == sign.POSITIVE, "Cannot apply elimination rule" 145 | assert isinstance(target.fml, forms.Negation), "Not a negation" 146 | 147 | target.sequent.derived_by = self 148 | target.subproof.derived_by = self 149 | 150 | # NOTE: Negation elimination requires a falsum in right 151 | falsum = next( 152 | filter(lambda x: str(x.fml) == "⊥", target.sequent.right), 153 | None, 154 | ) 155 | assert falsum, "`⊥` must be in conclusions" 156 | 157 | # NOTE: If you want to eliminate negation, you need to have its subformula 158 | subfml = target.fml.sub 159 | 160 | subfml = SequentItem(subfml, sign=sign.NEGATIVE, n=next(counter)) 161 | sequent = _apply(target, [subfml], counter, preserve_target=False) 162 | 163 | subfml = sequent.right[0] 164 | subfml.subproof = NDSubproof(subfml) 165 | 166 | # Look up falsum 167 | falsum = next( 168 | filter(lambda x: str(x.fml) == "⊥", target.sequent.right), 169 | None, 170 | ) 171 | 172 | falsum.subproof.children = [ 173 | subfml.subproof, 174 | target.subproof, 175 | ] 176 | 177 | new_falsum = next( 178 | filter(lambda x: str(x.fml) == "⊥", sequent.right), 179 | None, 180 | ) 181 | 182 | new_falsum.subproof = falsum.subproof 183 | 184 | return { 185 | "queue_items": [sequent], 186 | "counter": counter, 187 | } 188 | 189 | 190 | class Conjunction: 191 | # Intro = signed_rules.NegativeConjunctionRule 192 | # class Intro(signed_rules.NegativeConjunctionRule, IntroductionRule): 193 | # pass 194 | 195 | class Intro(IntroductionRule): 196 | label = "∧I" 197 | latex_label = r"$\land$I" 198 | 199 | def __init__(self): 200 | pass 201 | 202 | def apply(self, target: SequentItem, counter=count(1)): 203 | assert target.sign == sign.NEGATIVE, "Cannot apply introduction rule" 204 | assert isinstance(target.fml, forms.Conjunction), "Not a conjunction" 205 | 206 | target.sequent.derived_by = self 207 | target.subproof.derived_by = self 208 | 209 | branches = [] 210 | 211 | for conj in target.fml.subs: 212 | conj = SequentItem(conj, sign=sign.NEGATIVE, n=next(counter)) 213 | sequent = _apply(target, [conj], counter, preserve_target=False) 214 | branches.append(sequent) 215 | 216 | # Subproof 217 | for branch in branches: 218 | for item in branch.left: 219 | if getattr(item, "subproof", None) is None: 220 | item.subproof = NDSubproof(item) 221 | 222 | branch.right[0].subproof = NDSubproof(branch.right[0]) 223 | branch.right[0].subproof.children = [ 224 | deepcopy(item.subproof) for item in branch.left 225 | ] 226 | 227 | if branch.tautology(): 228 | left_item = next( 229 | filter( 230 | lambda x: str(x.fml) == str(branch.right[0]), branch.left 231 | ), 232 | None, 233 | ) 234 | branch.right[0].subproof.children = left_item.subproof.children 235 | 236 | target.sequent.right[0].subproof.children = [ 237 | branch.right[0].subproof for branch in branches 238 | ] 239 | 240 | return { 241 | "queue_items": branches, 242 | "counter": counter, 243 | } 244 | 245 | # TODO: Choice of conjunct 246 | # Elim = signed_rules.PositiveConjunctionRule 247 | class Elim(EliminationRule): 248 | label = "∧E" 249 | latex_label = r"$\land$E" 250 | 251 | def __init__(self, conjunct: Literal["left", "right"]): 252 | self.conjunct = conjunct 253 | 254 | def apply(self, target: SequentItem, counter=count(1)): 255 | assert target.sign == sign.POSITIVE, "Cannot apply elimination rule" 256 | assert isinstance(target.fml, forms.Conjunction), "Not a conjunction" 257 | 258 | target.sequent.derived_by = self 259 | target.subproof.derived_by = self 260 | 261 | conj1, conj2 = target.fml.subs 262 | if self.conjunct == "left": 263 | item = SequentItem(conj1, sign=sign.POSITIVE, n=next(counter)) 264 | elif self.conjunct == "right": 265 | item = SequentItem(conj2, sign=sign.POSITIVE, n=next(counter)) 266 | 267 | sq1, target = _apply(target, [item], counter) 268 | # sq2 = Sequent([target], [item], parent=target.sequent) 269 | 270 | # Subproof 271 | item.subproof = NDSubproof( 272 | item, 273 | children=[target.subproof], 274 | parent=target.sequent.right[0].subproof, 275 | ) 276 | 277 | return { 278 | "queue_items": [sq1], 279 | "counter": counter, 280 | } 281 | 282 | 283 | class Disjunction: 284 | # Intro = signed_rules.NegativeDisjunctionRule 285 | class Intro(IntroductionRule): 286 | label = "∨I" 287 | latex_label = r"$\lor$I" 288 | 289 | def __init__(self, disjunct: Literal["left", "right"]): 290 | self.disjunct = disjunct 291 | 292 | def apply(self, target: SequentItem, counter=count(1)): 293 | assert target.sign == sign.NEGATIVE, "Sign is not negative" 294 | assert isinstance(target.fml, forms.Disjunction), "Not a disjunction" 295 | 296 | target.sequent.derived_by = self 297 | target.subproof.derived_by = self 298 | 299 | disj1, disj2 = target.fml.subs 300 | 301 | if self.disjunct == "left": 302 | disjunct_item = SequentItem(disj1, sign=sign.NEGATIVE, n=next(counter)) 303 | elif self.disjunct == "right": 304 | disjunct_item = SequentItem(disj2, sign=sign.NEGATIVE, n=next(counter)) 305 | else: 306 | raise ValueError("Invalid disjunct") 307 | 308 | sq = _apply(target, [disjunct_item], counter, preserve_target=False) 309 | 310 | # Subproof 311 | disjunct_item.subproof = NDSubproof( 312 | disjunct_item, 313 | children=[deepcopy(left_item.subproof) for left_item in sq.left], 314 | ) 315 | 316 | target.subproof.children = [deepcopy(disjunct_item.subproof)] 317 | 318 | if sq.tautology(): 319 | left_item = next( 320 | filter(lambda x: str(x.fml) == str(disjunct_item.fml), sq.left), 321 | None, 322 | ) 323 | target.subproof.children = [deepcopy(left_item.subproof)] 324 | 325 | return { 326 | "queue_items": [sq], 327 | "counter": counter, 328 | } 329 | 330 | # Elim = signed_rules.PositiveDisjunctionRule 331 | class Elim(EliminationRule): 332 | label = "∨E" 333 | latex_label = r"$\lor$E" 334 | 335 | def __init__(self): 336 | pass 337 | 338 | def apply(self, target: SequentItem, counter=count(1)): 339 | assert target.sign == sign.POSITIVE, "Cannot apply elimination rule" 340 | assert isinstance(target.fml, forms.Disjunction), "Not a disjunction" 341 | 342 | target.sequent.derived_by = self 343 | target.subproof.derived_by = self 344 | 345 | branches = [] 346 | 347 | for disj in target.fml.subs: 348 | disj = SequentItem(disj, sign=sign.POSITIVE, n=next(counter)) 349 | sequent, _target = _apply(target, [disj], counter) 350 | branches.append(sequent) 351 | 352 | # Subproof 353 | for branch in branches: 354 | for left_item in branch.left: 355 | if getattr(left_item, "subproof", None) is None: 356 | left_item.subproof = NDSubproof(left_item) 357 | 358 | branch.right[0].subproof = NDSubproof( 359 | branch.right[0], 360 | # parent=target.sequent.right[0].subproof, 361 | parent=target.subproof, 362 | children=[deepcopy(item.subproof) for item in branch.left], 363 | ) 364 | 365 | target.sequent.right[0].subproof.children = [ 366 | branch.right[0].subproof for branch in branches 367 | ] + [target.subproof] 368 | 369 | return { 370 | "queue_items": branches, 371 | "counter": counter, 372 | } 373 | 374 | 375 | class Conditional: 376 | # class Intro(signed_rules.NegativeConditionalRule, IntroductionRule): 377 | # pass 378 | class Intro(IntroductionRule): 379 | label = "→I" 380 | latex_label = r"$\to$I" 381 | 382 | def apply(self, target: SequentItem, counter=count(1)): 383 | assert target.sign == sign.NEGATIVE, "Cannot apply introduction rule" 384 | assert isinstance(target.fml, forms.Conditional), "Not a conditional" 385 | 386 | target.sequent.derived_by = self 387 | target.subproof.derived_by = self 388 | 389 | antec, conseq = target.fml.subs 390 | 391 | antec = SequentItem(antec, sign=sign.POSITIVE, n=next(counter)) 392 | conseq = SequentItem( 393 | conseq, 394 | sign=sign.NEGATIVE, 395 | n=next(counter), 396 | ) 397 | sq = _apply(target, [antec, conseq], counter, preserve_target=False) 398 | 399 | # Subproof 400 | conseq.subproof = NDSubproof( 401 | conseq, 402 | children=[deepcopy(node.subproof) for node in target.sequent.left], 403 | ) 404 | target.sequent.right[0].subproof.children = [conseq.subproof] 405 | antec.subproof = NDSubproof( 406 | antec, 407 | parent=conseq.subproof, 408 | children=[], 409 | ) 410 | 411 | return { 412 | "queue_items": [sq], 413 | "counter": counter, 414 | } 415 | 416 | class Elim(EliminationRule): 417 | label = "→E" 418 | latex_label = r"$\to$E" 419 | 420 | def __init__(self): 421 | pass 422 | 423 | def apply(self, target: SequentItem, counter=count(1)): 424 | assert target.sign == sign.POSITIVE, "Cannot apply elimination rule" 425 | assert isinstance(target.fml, forms.Conditional), "Not a conditional" 426 | 427 | target.sequent.derived_by = self 428 | target.subproof.derived_by = self 429 | 430 | antec, conseq = target.fml.subs 431 | 432 | branches = [] 433 | 434 | antec = SequentItem(antec, sign=sign.NEGATIVE, n=next(counter)) 435 | 436 | sequent, _target_antec = _apply(target, [antec], counter) 437 | # TODO: This is a unnecesarilly complex way to do this 438 | # TODO: Fix dropped numbering 439 | new_items = [] 440 | for item in sequent.items: 441 | if item.sign == sign.POSITIVE or item.fml == antec.fml: 442 | new_items.append(item) 443 | sequent.items = new_items 444 | 445 | if sequent.tautology(): 446 | antec.subproof = NDSubproof(antec) 447 | else: 448 | antec.subproof = NDSubproof( 449 | antec, children=deepcopy(target.sequent.right[0].subproof.children) 450 | ) 451 | 452 | branches.append(sequent) 453 | 454 | conseq = SequentItem(conseq, sign=sign.POSITIVE, n=next(counter)) 455 | sequent, _target_conseq = _apply(target, [conseq], counter) 456 | 457 | # NOTE: Connect the subproofs 458 | sequent.right[0].subproof = target.sequent.right[0].subproof 459 | 460 | branches.append(sequent) 461 | 462 | new_subproof_antec = antec.subproof 463 | new_subproof_conditional = deepcopy(target.subproof) 464 | 465 | new_subproofs = [ 466 | new_subproof_antec, 467 | new_subproof_conditional, 468 | ] 469 | 470 | # TODO: Parent must be all right items 471 | conseq.subproof = NDSubproof( 472 | conseq, 473 | parent=target.sequent.right[0].subproof, 474 | children=new_subproofs, 475 | ) 476 | 477 | return { 478 | "queue_items": branches, 479 | "counter": counter, 480 | } 481 | -------------------------------------------------------------------------------- /mathesis/deduction/sequent_calculus/__init__.py: -------------------------------------------------------------------------------- 1 | from mathesis.deduction.sequent_calculus.sequent import Sequent, SequentItem 2 | from mathesis.deduction.sequent_calculus.sequent_tree import SequentTree 3 | from mathesis.deduction.sequent_calculus import rules 4 | 5 | __all__ = ["Sequent", "SequentItem", "SequentTree", "rules"] 6 | -------------------------------------------------------------------------------- /mathesis/deduction/sequent_calculus/rules.py: -------------------------------------------------------------------------------- 1 | from itertools import count 2 | 3 | from mathesis import forms 4 | from mathesis.deduction.sequent_calculus.sequent import Sequent, SequentItem, sign 5 | 6 | 7 | class Rule: 8 | label: str 9 | latex_label: str 10 | 11 | def __str__(self): 12 | return self.label 13 | 14 | def latex(self): 15 | return self.latex_label 16 | 17 | 18 | class StructuralRule(Rule): 19 | pass 20 | 21 | 22 | def _apply(target, new_items, counter): 23 | branch_items = new_items 24 | 25 | for item in target.sequent.items: 26 | if item != target: 27 | node = item.clone() 28 | node.n = next(counter) 29 | branch_items.append(node) 30 | 31 | branch_sequent = Sequent([], [], parent=target.sequent) 32 | branch_sequent.items = branch_items 33 | 34 | return branch_sequent 35 | 36 | 37 | class Negation: 38 | # Left = signed_rules.PositiveNegationRule 39 | class Left(Rule): 40 | label = "¬L" 41 | latex_label = r"$\neg$L" 42 | 43 | def apply(self, target, counter=count(1)): 44 | assert target.sign == sign.POSITIVE, "Not on the left side of sequent" 45 | assert isinstance(target.fml, forms.Negation), "Not a negation" 46 | 47 | target.sequent.derived_by = self 48 | 49 | subfml = target.fml.sub 50 | new_item = SequentItem(subfml.clone(), sign.NEGATIVE, n=next(counter)) 51 | branch_sequent = _apply(target, [new_item], counter) 52 | 53 | return { 54 | "queue_items": [branch_sequent], 55 | "counter": counter, 56 | } 57 | 58 | # Right = signed_rules.NegativeNegationRule 59 | class Right(Rule): 60 | label = "¬R" 61 | latex_label = r"$\neg$R" 62 | 63 | def apply(self, target, counter=count(1)): 64 | assert target.sign == sign.NEGATIVE, "Not on the right side of sequent" 65 | assert isinstance(target.fml, forms.Negation), "Not a negation" 66 | 67 | target.sequent.derived_by = self 68 | 69 | subfml = target.fml.sub 70 | new_item = SequentItem(subfml.clone(), sign.POSITIVE, n=next(counter)) 71 | branch_sequent = _apply(target, [new_item], counter) 72 | 73 | return { 74 | "queue_items": [branch_sequent], 75 | "counter": counter, 76 | } 77 | 78 | 79 | class Conjunction: 80 | # Left = signed_rules.PositiveConjunctionRule 81 | class Left(Rule): 82 | label = "∧L" 83 | latex_label = r"$\land$L" 84 | 85 | def apply(self, target, counter=count(1)): 86 | assert target.sign == sign.POSITIVE, "Not on the left side of sequent" 87 | assert isinstance(target.fml, forms.Conjunction), "Not a conjunction" 88 | 89 | target.sequent.derived_by = self 90 | 91 | conj1, conj2 = target.fml.subs 92 | conj1 = SequentItem(conj1.clone(), sign.POSITIVE, n=next(counter)) 93 | conj2 = SequentItem(conj2.clone(), sign.POSITIVE, n=next(counter)) 94 | branch_sequent = _apply(target, [conj1, conj2], counter) 95 | 96 | return { 97 | "queue_items": [branch_sequent], 98 | "counter": counter, 99 | } 100 | 101 | # Right = signed_rules.NegativeConjunctionRule 102 | class Right(Rule): 103 | label = "∧R" 104 | latex_label = r"$\land$R" 105 | 106 | def apply(self, target, counter=count(1)): 107 | assert target.sign == sign.NEGATIVE, "Not on the right side of sequent" 108 | assert isinstance(target.fml, forms.Conjunction), "Not a conjunction" 109 | 110 | target.sequent.derived_by = self 111 | 112 | branches = [] 113 | 114 | for conj in target.fml.subs: 115 | conj = SequentItem(conj.clone(), sign.NEGATIVE, n=next(counter)) 116 | branch_sequent = _apply(target, [conj], counter) 117 | branches.append(branch_sequent) 118 | 119 | return { 120 | "queue_items": branches, 121 | "counter": counter, 122 | } 123 | 124 | 125 | class Disjunction: 126 | # Left = signed_rules.PositiveDisjunctionRule 127 | class Left(Rule): 128 | label = "∨L" 129 | latex_label = r"$\lor$L" 130 | 131 | def apply(self, target, counter=count(1)): 132 | assert target.sign == sign.POSITIVE, "Not on the left side of sequent" 133 | assert isinstance(target.fml, forms.Disjunction), "Not a disjunction" 134 | 135 | target.sequent.derived_by = self 136 | 137 | branches = [] 138 | 139 | for subfml in target.fml.subs: 140 | new_item = SequentItem(subfml.clone(), sign.POSITIVE, n=next(counter)) 141 | branch_sequent = _apply(target, [new_item], counter) 142 | branches.append(branch_sequent) 143 | 144 | return { 145 | "queue_items": branches, 146 | "counter": counter, 147 | } 148 | 149 | # Right = signed_rules.NegativeDisjunctionRule 150 | class Right(Rule): 151 | label = "∨R" 152 | latex_label = r"$\lor$R" 153 | 154 | def apply(self, target, counter=count(1)): 155 | assert target.sign == sign.NEGATIVE, "Not on the right side of sequent" 156 | assert isinstance(target.fml, forms.Disjunction), "Not a disjunction" 157 | 158 | target.sequent.derived_by = self 159 | 160 | disj1, disj2 = target.fml.subs 161 | disj1 = SequentItem(disj1.clone(), sign.NEGATIVE, n=next(counter)) 162 | disj2 = SequentItem(disj2.clone(), sign.NEGATIVE, n=next(counter)) 163 | branch_sequent = _apply(target, [disj1, disj2], counter) 164 | 165 | return { 166 | "queue_items": [branch_sequent], 167 | "counter": counter, 168 | } 169 | 170 | 171 | class Conditional: 172 | # Left = signed_rules.PositiveConditionalRule 173 | class Left(Rule): 174 | label = "→L" 175 | latex_label = r"$\to$L" 176 | 177 | def apply(self, target, counter=count(1)): 178 | assert target.sign == sign.POSITIVE, "Not on the left side of sequent" 179 | assert isinstance(target.fml, forms.Conditional), "Not a conditional" 180 | 181 | target.sequent.derived_by = self 182 | 183 | antec, conseq = target.fml.subs 184 | antec = SequentItem(antec.clone(), sign.NEGATIVE, n=next(counter)) 185 | branch_sequent_1 = _apply(target, [antec], counter) 186 | conseq = SequentItem(conseq.clone(), sign.POSITIVE, n=next(counter)) 187 | branch_sequent_2 = _apply(target, [conseq], counter) 188 | 189 | return { 190 | "queue_items": [branch_sequent_1, branch_sequent_2], 191 | "counter": counter, 192 | } 193 | 194 | # Right = signed_rules.NegativeConditionalRule 195 | class Right(Rule): 196 | label = "→R" 197 | latex_label = r"$\to$R" 198 | 199 | def apply(self, target, counter=count(1)): 200 | assert target.sign == sign.NEGATIVE, "Not on the right side of sequent" 201 | assert isinstance(target.fml, forms.Conditional), "Not a conditional" 202 | 203 | target.sequent.derived_by = self 204 | 205 | antec, conseq = target.fml.subs 206 | antec = SequentItem(antec.clone(), sign.POSITIVE, n=next(counter)) 207 | conseq = SequentItem(conseq.clone(), sign.NEGATIVE, n=next(counter)) 208 | branch_sequent = _apply(target, [antec, conseq], counter) 209 | 210 | return { 211 | "queue_items": [branch_sequent], 212 | "counter": counter, 213 | } 214 | 215 | 216 | class Weakening: 217 | class Left(StructuralRule): 218 | label = "wL" 219 | latex_label = r"$wL" 220 | 221 | def apply(self, target, counter=count(1)): 222 | assert target.sign == sign.POSITIVE, "Not on the left side of sequent" 223 | 224 | target.sequent.derived_by = self 225 | 226 | branch_sequent = _apply(target, [], counter) 227 | 228 | return { 229 | "queue_items": [branch_sequent], 230 | "counter": counter, 231 | } 232 | 233 | class Right(StructuralRule): 234 | label = "wR" 235 | latex_label = r"$wR" 236 | 237 | def apply(self, target, counter=count(1)): 238 | assert target.sign == sign.NEGATIVE, "Not on the right side of sequent" 239 | 240 | target.sequent.derived_by = self 241 | 242 | branch_sequent = _apply(target, [], counter) 243 | 244 | return { 245 | "queue_items": [branch_sequent], 246 | "counter": counter, 247 | } 248 | 249 | 250 | class Contraction: 251 | # TODO 252 | pass 253 | 254 | 255 | class Exchange: 256 | # TODO 257 | pass 258 | -------------------------------------------------------------------------------- /mathesis/deduction/sequent_calculus/sequent.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from copy import deepcopy 4 | from types import SimpleNamespace 5 | from typing import List 6 | 7 | from anytree import NodeMixin 8 | 9 | from mathesis.deduction import sequent_calculus 10 | from mathesis.forms import Formula 11 | 12 | sign = SimpleNamespace( 13 | **{ 14 | "POSITIVE": "True", 15 | "NEGATIVE": "False", 16 | } 17 | ) 18 | 19 | 20 | class SequentItem: 21 | n: int | None 22 | sequent: Sequent | None 23 | 24 | def __init__( 25 | self, fml: Formula, sign, n: int | None = None, sequent: Sequent | None = None 26 | ): 27 | self.fml = fml 28 | self.sign = sign 29 | self.n = n 30 | self.sequent = sequent 31 | 32 | def clone(self): 33 | clone = deepcopy(self) 34 | clone.__dict__ = deepcopy(clone.__dict__) 35 | return clone 36 | 37 | @property 38 | def name(self): 39 | return str(self) 40 | 41 | def __str__(self) -> str: 42 | return str(self.fml) 43 | 44 | 45 | class Sequent(NodeMixin): 46 | """A sequent is a pair of premises and conclusions.""" 47 | 48 | __items: list 49 | derived_by: sequent_calculus.rules.Rule | None 50 | # parent: Sequent | None 51 | # children: List[Sequent] 52 | 53 | def __init__( 54 | self, 55 | left: List[Formula], 56 | right: List[Formula], 57 | parent: Sequent | None = None, 58 | children: list[Sequent] | None = None, 59 | ): 60 | super().__init__() 61 | self.__items = [] 62 | 63 | initial_items = [] 64 | for fml in left: 65 | item = SequentItem(fml, sign.POSITIVE) 66 | initial_items.append(item) 67 | for fml in right: 68 | item = SequentItem(fml, sign.NEGATIVE) 69 | initial_items.append(item) 70 | 71 | self.items = initial_items 72 | self.parent = parent 73 | if children: 74 | self.children = children 75 | 76 | self.derived_by = None 77 | 78 | def __getitem__(self, index): 79 | if index == 0: 80 | return self.left 81 | elif index == 1: 82 | return self.right 83 | 84 | @property 85 | def items(self): 86 | return self.__items 87 | 88 | @items.setter 89 | def items(self, value): 90 | for item in value: 91 | item.sequent = self 92 | self.__items = value 93 | 94 | @property 95 | def name(self): 96 | return str(self) 97 | 98 | @property 99 | def left(self) -> list[SequentItem]: 100 | return [item for item in self.items if item.sign == sign.POSITIVE] 101 | 102 | @property 103 | def right(self) -> list[SequentItem]: 104 | return [item for item in self.items if item.sign == sign.NEGATIVE] 105 | 106 | # @property 107 | def tautology(self): 108 | left = set(str(item.fml) for item in self.left) 109 | right = set(str(item.fml) for item in self.right) 110 | if left.intersection(right): 111 | return True 112 | else: 113 | return False 114 | 115 | def latex(self, arrow=r"\Rightarrow"): 116 | return "{} {} {}".format( 117 | ", ".join(map(lambda x: f"{x.fml.latex()}", self.left)), 118 | arrow, 119 | ", ".join(map(lambda x: f"{x.fml.latex()}", self.right)), 120 | ) 121 | 122 | def __str__(self): 123 | return "{} ⇒ {}".format( 124 | ", ".join(map(lambda x: f"{x.name}", self.left)), 125 | ", ".join(map(lambda x: f"{x.name}", self.right)), 126 | ) 127 | -------------------------------------------------------------------------------- /mathesis/deduction/sequent_calculus/sequent_tree.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from itertools import count 4 | from operator import itemgetter 5 | 6 | from anytree import PostOrderIter, RenderTree 7 | 8 | from mathesis.deduction.sequent_calculus.sequent import Sequent, SequentItem 9 | from mathesis.forms import Formula 10 | 11 | 12 | class SequentTree: 13 | """A tree of sequents.""" 14 | 15 | def __init__(self, premises: list[Formula], conclusions: list[Formula]): 16 | self.counter = count(1) 17 | self.bookkeeper = dict() 18 | left, right = (premises, conclusions) 19 | sequent = Sequent(left, right) 20 | for item in sequent.items: 21 | item.n = next(self.counter) 22 | self.bookkeeper[item.n] = item 23 | self.root = sequent 24 | 25 | def __getitem__(self, index): 26 | return self.bookkeeper[index] 27 | 28 | def _apply(self, target: SequentItem, rule): 29 | queue_items = itemgetter("queue_items")(rule.apply(target, self.counter)) 30 | 31 | new_sequents = [] 32 | 33 | for branch in queue_items: 34 | for node in branch.items: 35 | self.bookkeeper[node.n] = node 36 | branch.parent = target.sequent 37 | new_sequents.append(branch) 38 | 39 | return new_sequents 40 | 41 | def apply(self, target: SequentItem, rule): 42 | self._apply(target, rule) 43 | return self 44 | 45 | def tree(self, number=True): 46 | output = "" 47 | for pre, fill, node in RenderTree(self.root): 48 | output += "{}{}\n".format( 49 | pre, 50 | "{} ⇒ {}{}".format( 51 | ", ".join( 52 | map( 53 | lambda x: str(x) + (f" {x.n}" if number else ""), 54 | node.left, 55 | ) 56 | ), 57 | ", ".join( 58 | map( 59 | lambda x: str(x) + (f" {x.n}" if number else ""), 60 | node.right, 61 | ) 62 | ), 63 | f" [{node.derived_by}]" if node.derived_by is not None else "", 64 | ), 65 | ) 66 | return output 67 | 68 | def latex(self, number=False, arrow=r"\Rightarrow"): 69 | output = "" 70 | for node in PostOrderIter(self.root): 71 | if node.derived_by is not None and hasattr(node.derived_by, "latex"): 72 | label_part = f"\\RightLabel{{{node.derived_by.latex()}}}\n" 73 | else: 74 | label_part = "" 75 | 76 | tmpl = "" 77 | if len(node.children) == 0: 78 | tmpl += r"\AxiomC{{${}$}}" 79 | elif len(node.children) == 1: 80 | tmpl += r"\UnaryInfC{{${}$}}" 81 | elif len(node.children) == 2: 82 | tmpl += r"\BinaryInfC{{${}$}}" 83 | output += label_part + tmpl.format(node.latex()) + "\n" 84 | return """\\begin{{prooftree}}\n{}\\end{{prooftree}}""".format(output) 85 | -------------------------------------------------------------------------------- /mathesis/deduction/tableau/__init__.py: -------------------------------------------------------------------------------- 1 | from mathesis.deduction.tableau.tableau import * 2 | -------------------------------------------------------------------------------- /mathesis/deduction/tableau/rules.py: -------------------------------------------------------------------------------- 1 | from anytree import Node, Walker 2 | from itertools import count 3 | from copy import copy 4 | 5 | from mathesis import forms 6 | 7 | 8 | class Rule: 9 | pass 10 | 11 | 12 | class DoubleNegationRule(Rule): 13 | def apply(self, target, tip, counter=count(1)): 14 | """ 15 | Args: 16 | target: a node 17 | tip: a leaf node of the branch to extend 18 | counter: a counter 19 | """ 20 | subsubfml = target.fml.sub.sub 21 | node = Node( 22 | str(subsubfml), sign=target.sign, fml=subsubfml, parent=tip, n=next(counter) 23 | ) 24 | return { 25 | "queue_items": [node], 26 | "counter": counter, 27 | } 28 | 29 | 30 | class ConjunctionRule(Rule): 31 | def apply(self, target, tip, counter=count(1)): 32 | conj1, conj2 = target.fml.subs 33 | node1 = Node( 34 | str(conj1), sign=target.sign, fml=conj1, parent=tip, n=next(counter) 35 | ) 36 | node2 = Node( 37 | str(conj2), sign=target.sign, fml=conj2, parent=node1, n=next(counter) 38 | ) 39 | return { 40 | "queue_items": [node1, node2], 41 | } 42 | 43 | 44 | class NegatedConjunctionRule(Rule): 45 | def apply(self, target, tip, counter=count(1)): 46 | conj1, conj2 = target.fml.sub.subs 47 | nconj1, nconj2 = map(lambda v: forms.Negation(v), [conj1, conj2]) 48 | nodeL = Node( 49 | str(nconj1), sign=target.sign, fml=nconj1, parent=tip, n=next(counter) 50 | ) 51 | nodeR = Node( 52 | str(nconj2), sign=target.sign, fml=nconj2, parent=tip, n=next(counter) 53 | ) 54 | return { 55 | "queue_items": [nodeL, nodeR], 56 | "counter": counter, 57 | } 58 | 59 | 60 | class DisjunctionRule(Rule): 61 | def apply(self, target, tip, counter=count(1)): 62 | disj1, disj2 = target.fml.subs 63 | nodeL = Node( 64 | str(disj1), sign=target.sign, fml=disj1, parent=tip, n=next(counter) 65 | ) 66 | nodeR = Node( 67 | str(disj2), sign=target.sign, fml=disj2, parent=tip, n=next(counter) 68 | ) 69 | return { 70 | "queue_items": [nodeL, nodeR], 71 | "counter": counter, 72 | } 73 | 74 | 75 | class NegatedDisjunctionRule(Rule): 76 | def apply(self, target, tip, counter=count(1)): 77 | disj1, disj2 = target.fml.sub.subs 78 | ndisj1, ndisj2 = map(lambda v: forms.Negation(v), [disj1, disj2]) 79 | node1 = Node( 80 | str(ndisj1), sign=target.sign, fml=ndisj1, parent=tip, n=next(counter) 81 | ) 82 | node2 = Node( 83 | str(ndisj2), sign=target.sign, fml=ndisj2, parent=node1, n=next(counter) 84 | ) 85 | return { 86 | "queue_items": [node1, node2], 87 | "counter": counter, 88 | } 89 | 90 | 91 | class ConditionalRule(Rule): 92 | def apply(self, target, tip, counter=count(1)): 93 | antec, conseq = target.fml.subs 94 | nantec = forms.Negation(antec) 95 | nodeL = Node( 96 | str(nantec), sign=target.sign, fml=nantec, parent=tip, n=next(counter) 97 | ) 98 | nodeR = Node( 99 | str(conseq), sign=target.sign, fml=conseq, parent=tip, n=next(counter) 100 | ) 101 | return { 102 | "queue_items": [nodeL, nodeR], 103 | "counter": counter, 104 | } 105 | 106 | 107 | class NegatedConditionalRule(Rule): 108 | def apply(self, target, tip, counter=count(1)): 109 | antec, conseq = target.fml.sub.subs 110 | nconseq = forms.Negation(conseq) 111 | node1 = Node( 112 | str(antec), sign=target.sign, fml=antec, parent=tip, n=next(counter) 113 | ) 114 | node2 = Node( 115 | str(nconseq), sign=target.sign, fml=nconseq, parent=node1, n=next(counter) 116 | ) 117 | return { 118 | "queue_items": [node1, node2], 119 | "counter": counter, 120 | } 121 | 122 | 123 | class UniversalInstantiationRule(Rule): 124 | def __init__(self, replacing_term): 125 | self.replacing_term = replacing_term 126 | 127 | def apply(self, target, tip, counter=count(1)): 128 | target_variable = target.fml.variable 129 | # TODO: copy recusrively 130 | subfml = target.fml.sub.clone() 131 | # TODO: check if a valid replacing_term 132 | for term in subfml.free_terms: 133 | if term == target_variable: 134 | subfml = subfml.replace_term(term, self.replacing_term) 135 | # print(target_variable, subfml.terms) 136 | node = Node( 137 | str(subfml), 138 | sign=target.sign, 139 | fml=subfml, 140 | parent=tip, 141 | n=next(counter), 142 | ) 143 | return { 144 | "queue_items": [[node]], 145 | "counter": counter, 146 | } 147 | 148 | 149 | class NegatedUniversalRule(Rule): 150 | def apply(self, target, tip, counter=count(1)): 151 | subfml = target.fml.sub 152 | subsubfml = subfml.sub.clone() 153 | fml = forms.Particular(subfml.variable, forms.Negation(subsubfml)) 154 | node = Node( 155 | str(fml), 156 | sign=target.sign, 157 | fml=fml, 158 | parent=tip, 159 | n=next(counter), 160 | ) 161 | return { 162 | "queue_items": [node], 163 | "counter": counter, 164 | } 165 | 166 | 167 | class ParticularInstantiationRule(Rule): 168 | def __init__(self, replacing_term): 169 | self.replacing_term = replacing_term 170 | 171 | def apply(self, target, tip, counter=count(1)): 172 | target_variable = target.fml.variable 173 | # TODO: copy recusrively 174 | subfml = target.fml.sub.clone() 175 | 176 | # NOTE: check if a valid replacing_term 177 | ancestors = (tip,) + tip.ancestors 178 | for ancestor in ancestors: 179 | fml = ancestor.fml 180 | # print(fml, fml.free_terms) 181 | assert self.replacing_term not in fml.free_terms, "Replacing term has been already occurred in the branch" 182 | 183 | for term in subfml.free_terms: 184 | if term == target_variable: 185 | subfml.replace_term(term, self.replacing_term) 186 | node = Node( 187 | str(subfml), 188 | sign=target.sign, 189 | fml=subfml, 190 | parent=tip, 191 | n=next(counter), 192 | ) 193 | return { 194 | "queue_items": [[node]], 195 | "counter": counter, 196 | } 197 | 198 | 199 | class NegatedParticularRule(Rule): 200 | def apply(self, target, tip, counter=count(1)): 201 | subfml = target.fml.sub 202 | subsubfml = subfml.sub.clone() 203 | fml = forms.Universal(subfml.variable, forms.Negation(subsubfml)) 204 | node = Node( 205 | str(fml), 206 | sign=target.sign, 207 | fml=fml, 208 | parent=tip, 209 | n=next(counter), 210 | ) 211 | return { 212 | "queue_items": [[node]], 213 | "counter": counter, 214 | } 215 | -------------------------------------------------------------------------------- /mathesis/deduction/tableau/signed_rules.py: -------------------------------------------------------------------------------- 1 | from anytree import Node 2 | from itertools import count 3 | 4 | from mathesis import forms 5 | from mathesis.deduction.tableau import sign 6 | 7 | 8 | class Rule: 9 | pass 10 | 11 | 12 | class PositiveNegationRule(Rule): 13 | def apply(self, target, tip, counter=count(1)): 14 | assert target.sign == sign.POSITIVE, "Sign is not positive" 15 | assert isinstance(target.fml, forms.Negation), "Not a negation" 16 | subfml = target.fml.sub 17 | node = Node( 18 | str(subfml), sign=sign.NEGATIVE, fml=subfml, parent=tip, n=next(counter) 19 | ) 20 | return { 21 | "queue_items": [[node]], 22 | "counter": counter, 23 | } 24 | 25 | 26 | class NegativeNegationRule(Rule): 27 | def apply(self, target, tip, counter=count(1)): 28 | assert target.sign == sign.NEGATIVE, "Sign is not negative" 29 | assert isinstance(target.fml, forms.Negation), "Not a negation" 30 | subfml = target.fml.sub 31 | node = Node( 32 | str(subfml), sign=sign.POSITIVE, fml=subfml, parent=tip, n=next(counter) 33 | ) 34 | return { 35 | "queue_items": [[node]], 36 | "counter": counter, 37 | } 38 | 39 | 40 | class NegationRule(Rule): 41 | def apply(self, target, tip, counter=count(1)): 42 | if target.sign == sign.POSITIVE: 43 | return PositiveNegationRule().apply(target, tip, counter=counter) 44 | elif target.sign == sign.NEGATIVE: 45 | return NegativeNegationRule().apply(target, tip, counter=counter) 46 | 47 | 48 | class PositiveConjunctionRule(Rule): 49 | def apply(self, target, tip, counter=count(1)): 50 | assert target.sign == sign.POSITIVE, "Sign is not positive" 51 | assert isinstance(target.fml, forms.Conjunction), "Not a conjunction" 52 | conj1, conj2 = target.fml.subs 53 | node1 = Node( 54 | str(conj1), sign=target.sign, fml=conj1, parent=tip, n=next(counter) 55 | ) 56 | node2 = Node( 57 | str(conj2), sign=target.sign, fml=conj2, parent=node1, n=next(counter) 58 | ) 59 | return { 60 | "queue_items": [[node1, node2]], 61 | } 62 | 63 | 64 | class NegativeConjunctionRule(Rule): 65 | def apply(self, target, tip, counter=count(1)): 66 | assert target.sign == sign.NEGATIVE, "Sign is not negative" 67 | assert isinstance(target.fml, forms.Conjunction), "Not a conjunction" 68 | conj1, conj2 = target.fml.subs 69 | nodeL = Node( 70 | str(conj1), sign=sign.NEGATIVE, fml=conj1, parent=tip, n=next(counter) 71 | ) 72 | nodeR = Node( 73 | str(conj2), sign=sign.NEGATIVE, fml=conj2, parent=tip, n=next(counter) 74 | ) 75 | return { 76 | "queue_items": [[nodeL], [nodeR]], 77 | "counter": counter, 78 | } 79 | 80 | 81 | class ConjunctionRule(Rule): 82 | def apply(self, target, tip, counter=count(1)): 83 | if target.sign == sign.POSITIVE: 84 | return PositiveConjunctionRule().apply(target, tip, counter=counter) 85 | elif target.sign == sign.NEGATIVE: 86 | return NegativeConjunctionRule().apply(target, tip, counter=counter) 87 | 88 | 89 | class PositiveDisjunctionRule(Rule): 90 | def apply(self, target, tip, counter=count(1)): 91 | assert target.sign == sign.POSITIVE, "Sign is not positive" 92 | assert isinstance(target.fml, forms.Disjunction), "Not a disjunction" 93 | disj1, disj2 = target.fml.subs 94 | nodeL = Node( 95 | str(disj1), sign=target.sign, fml=disj1, parent=tip, n=next(counter) 96 | ) 97 | nodeR = Node( 98 | str(disj2), sign=target.sign, fml=disj2, parent=tip, n=next(counter) 99 | ) 100 | return { 101 | "queue_items": [[nodeL], [nodeR]], 102 | "counter": counter, 103 | } 104 | 105 | 106 | class NegativeDisjunctionRule(Rule): 107 | def apply(self, target, tip, counter=count(1)): 108 | assert target.sign == sign.NEGATIVE, "Sign is not negative" 109 | assert isinstance(target.fml, forms.Disjunction), "Not a disjunction" 110 | disj1, disj2 = target.fml.subs 111 | node1 = Node( 112 | str(disj1), sign=sign.NEGATIVE, fml=disj1, parent=tip, n=next(counter) 113 | ) 114 | node2 = Node( 115 | str(disj2), sign=sign.NEGATIVE, fml=disj2, parent=node1, n=next(counter) 116 | ) 117 | return { 118 | "queue_items": [[node1, node2]], 119 | "counter": counter, 120 | } 121 | 122 | 123 | class DisjunctionRule(Rule): 124 | def apply(self, target, tip, counter=count(1)): 125 | if target.sign == sign.POSITIVE: 126 | return PositiveDisjunctionRule().apply(target, tip, counter=counter) 127 | elif target.sign == sign.NEGATIVE: 128 | return NegativeDisjunctionRule().apply(target, tip, counter=counter) 129 | 130 | 131 | class PositiveConditionalRule(Rule): 132 | def apply(self, target, tip, counter=count(1)): 133 | assert target.sign == sign.POSITIVE, "Sign is not positive" 134 | assert isinstance(target.fml, forms.Conditional), "Not a conditional" 135 | antec, desc = target.fml.subs 136 | nodeL = Node( 137 | str(antec), sign=sign.NEGATIVE, fml=antec, parent=tip, n=next(counter) 138 | ) 139 | nodeR = Node( 140 | str(desc), sign=sign.POSITIVE, fml=desc, parent=tip, n=next(counter) 141 | ) 142 | return { 143 | "queue_items": [[nodeL], [nodeR]], 144 | "counter": counter, 145 | } 146 | 147 | 148 | class NegativeConditionalRule(Rule): 149 | def apply(self, target, tip, counter=count(1)): 150 | assert target.sign == sign.NEGATIVE, "Sign is not negative" 151 | assert isinstance(target.fml, forms.Conditional), "Not a conditional" 152 | antec, desc = target.fml.subs 153 | node1 = Node( 154 | str(antec), sign=sign.POSITIVE, fml=antec, parent=tip, n=next(counter) 155 | ) 156 | node2 = Node( 157 | str(desc), sign=sign.NEGATIVE, fml=desc, parent=node1, n=next(counter) 158 | ) 159 | return { 160 | "queue_items": [[node1, node2]], 161 | "counter": counter, 162 | } 163 | 164 | 165 | class ConditionalRule(Rule): 166 | def apply(self, target, tip, counter=count(1)): 167 | if target.sign == sign.POSITIVE: 168 | return PositiveConditionalRule().apply(target, tip, counter=counter) 169 | elif target.sign == sign.NEGATIVE: 170 | return NegativeConditionalRule().apply(target, tip, counter=counter) 171 | -------------------------------------------------------------------------------- /mathesis/deduction/tableau/tableau.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from itertools import count 3 | from operator import itemgetter, neg 4 | from types import SimpleNamespace 5 | from typing import Any, List 6 | 7 | from anytree import Node, RenderTree, find_by_attr 8 | 9 | from mathesis import _utils, forms 10 | from mathesis.deduction.tableau import rules 11 | 12 | sign = SimpleNamespace( 13 | **{ 14 | "POSITIVE": "True", 15 | "NEGATIVE": "False", 16 | } 17 | ) 18 | 19 | logger = logging.getLogger(__name__) 20 | logger.addHandler(logging.NullHandler()) 21 | 22 | 23 | class SignedTableau: 24 | """A signed tableau is a tree of formulas with signs.""" 25 | 26 | counter = count(1) 27 | 28 | def __init__( 29 | self, premises: List[forms.Formula], conclusions: List[forms.Formula] = [] 30 | ): 31 | self.counter = count(1) 32 | self.root = None 33 | parent = None 34 | for fml in premises: 35 | node = Node( 36 | str(fml), 37 | sign=sign.POSITIVE, 38 | fml=fml, 39 | n=next(self.counter), 40 | ) 41 | if not self.root: 42 | self.root = node 43 | if parent: 44 | node.parent = parent 45 | parent = node 46 | for fml in conclusions: 47 | node = Node( 48 | str(fml), 49 | sign=sign.NEGATIVE, 50 | fml=fml, 51 | n=next(self.counter), 52 | ) 53 | if not self.root: 54 | self.root = node 55 | if parent: 56 | node.parent = parent 57 | parent = node 58 | 59 | def __getitem__(self, index): 60 | return find_by_attr(self.root, name="n", value=index) 61 | 62 | def check_close(self, node1, node2): 63 | return node1.name == node2.name and node1.sign != node2.sign 64 | 65 | def is_closed(self): 66 | logger.debug(("leaves: %s", self.root.leaves)) 67 | if all(getattr(node, "branch_marked", False) for node in self.root.leaves): 68 | return True 69 | return False 70 | 71 | def apply(self, target: Node, rule: rules.Rule): 72 | self.apply_and_queue(target, rule) 73 | return self 74 | 75 | def apply_and_queue(self, target: Node, rule: rules.Rule, all=True, flatten=True): 76 | branch_tips = [ 77 | leaf for leaf in target.leaves if not getattr(leaf, "branch_marked", False) 78 | ] 79 | queue_items = [] 80 | queue_items_all = [] 81 | for tip in branch_tips: 82 | queue_items = itemgetter("queue_items")( 83 | rule.apply(target, tip, self.counter) 84 | ) 85 | # NOTE: check if newly queued nodes are to be marked 86 | flattened_queue_items = list(_utils.flatten_list(queue_items)) 87 | for node in flattened_queue_items: 88 | ancestor = node.parent 89 | while ancestor is not None: 90 | if self.check_close(node, ancestor): 91 | node.marked = True 92 | node.branch_marked = True 93 | for desc in node.descendants: 94 | desc.branch_marked = True 95 | break 96 | ancestor = ancestor.parent 97 | queue_items_all += flattened_queue_items 98 | return queue_items_all if all else queue_items 99 | 100 | def htree(self): 101 | output = "" 102 | for pre, fill, node in RenderTree(self.root): 103 | output += "{}{} {} {}{}\n".format( 104 | pre, 105 | node.sign, 106 | node.name, 107 | node.n, 108 | " ×" if getattr(node, "marked", False) else "", 109 | ) 110 | return output 111 | 112 | def tree(self): 113 | return self.htree() 114 | 115 | 116 | class Tableau(SignedTableau): 117 | """An (unsigned) tableau is a tree of formulas without signs.""" 118 | 119 | def __init__( 120 | self, premises: List[forms.Formula], conclusions: List[forms.Formula] = [] 121 | ): 122 | if not all(isinstance(fml, forms.Formula) for fml in premises + conclusions): 123 | raise Exception("All premises and conclusions must be formulas.") 124 | 125 | self.counter = count(1) 126 | self.root = None 127 | parent = None 128 | for fml in premises: 129 | node = Node( 130 | str(fml), 131 | sign=sign.POSITIVE, 132 | fml=fml, 133 | n=next(self.counter), 134 | ) 135 | if not self.root: 136 | self.root = node 137 | if parent: 138 | node.parent = parent 139 | parent = node 140 | for fml in conclusions: 141 | neg_fml = forms.Negation(fml) 142 | node = Node( 143 | str(neg_fml), 144 | sign=sign.POSITIVE, 145 | fml=neg_fml, 146 | n=next(self.counter), 147 | ) 148 | if not self.root: 149 | self.root = node 150 | if parent: 151 | node.parent = parent 152 | parent = node 153 | 154 | def check_close(self, node1, node2): 155 | return ( 156 | node1.name == str(forms.Negation(node2.fml)) 157 | or str(forms.Negation(node1.fml)) == node2.name 158 | ) 159 | 160 | def htree(self): 161 | output = "" 162 | for pre, fill, node in RenderTree(self.root): 163 | output += "{}{} {}{}\n".format( 164 | pre, node.name, node.n, " ×" if getattr(node, "marked", False) else "" 165 | ) 166 | return output 167 | -------------------------------------------------------------------------------- /mathesis/forms.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Optional 3 | 4 | from copy import copy, deepcopy 5 | 6 | 7 | class Formula: 8 | @property 9 | def atom_symbols(self): 10 | return list(self.atoms.keys()) 11 | 12 | def transform(self, transformer): 13 | return transformer(self) 14 | 15 | def __eq__(self, other) -> bool: 16 | if not isinstance(other, Formula): 17 | return NotImplemented 18 | 19 | elif not isinstance(other, type(self)): # not the same (main) connective 20 | return False 21 | 22 | return str(self) == str(other) 23 | 24 | 25 | class Atom(Formula): 26 | _latex: str | None 27 | 28 | def __init__(self, predicate: str, terms: list[str]=[], latex: Optional[str]=None): 29 | self.predicate = predicate 30 | self.terms = terms 31 | self._latex = latex 32 | 33 | @property 34 | def symbol(self) -> str: 35 | if len(self.terms) == 0: 36 | return f"{self.predicate}" 37 | else: 38 | return f"{self.predicate}({', '.join(self.terms)})" 39 | 40 | @property 41 | def atoms(self): 42 | return {self.symbol: [self]} 43 | 44 | @property 45 | def free_terms(self): 46 | return self.terms 47 | 48 | def replace_term(self, replaced_term, replacing_term): 49 | fml = self 50 | fml.terms = [ 51 | replacing_term if term == replaced_term else term for term in self.terms 52 | ] 53 | return fml 54 | 55 | def clone(self): 56 | clone = deepcopy(self) 57 | clone.__dict__ = deepcopy(clone.__dict__) 58 | return clone 59 | 60 | def latex(self): 61 | return f"{self._latex or self.symbol}" 62 | 63 | def __str__(self) -> str: 64 | return f"{self.symbol}" 65 | 66 | def __repr__(self) -> str: 67 | return f"Atom[{self.symbol}]" 68 | 69 | 70 | class Constant(Atom): 71 | """Base class for Top and Bottom""" 72 | 73 | signature: str 74 | connective: str 75 | connective_latex: str 76 | 77 | def __init__(self): 78 | pass 79 | 80 | @property 81 | def symbol(self) -> str: 82 | return self.connective 83 | 84 | @property 85 | def atoms(self): 86 | return {} 87 | 88 | @property 89 | def free_terms(self): 90 | return [] 91 | 92 | def replace_term(self, replaced_term, replacing_term): 93 | return self 94 | 95 | def latex(self) -> str: 96 | return self.connective_latex 97 | 98 | def __str__(self) -> str: 99 | return self.symbol 100 | 101 | def __repr__(self) -> str: 102 | return self.signature 103 | 104 | 105 | class Top(Constant): 106 | signature = "Top" 107 | connective = "⊤" 108 | connective_latex = r"\top" 109 | 110 | 111 | class Bottom(Constant): 112 | signature = "Bottom" 113 | connective = "⊥" 114 | connective_latex = r"\bot" 115 | 116 | 117 | class Unary(Formula): 118 | def __init__(self, sub: Formula): 119 | self.sub = sub 120 | 121 | def clone(self): 122 | clone = deepcopy(self.sub.clone()) 123 | clone.__dict__ = deepcopy(clone.__dict__) 124 | return self.__class__(clone) 125 | 126 | @property 127 | def atoms(self): 128 | return self.sub.atoms 129 | 130 | @property 131 | def free_terms(self): 132 | return self.sub.free_terms 133 | 134 | def replace_term(self, replaced_term, replacing_term): 135 | fml = self 136 | fml.sub = self.sub.replace_term(replaced_term, replacing_term) 137 | return fml 138 | 139 | def latex(self): 140 | return f"{self.connective_latex} " + ( 141 | f"({self.sub.latex()})" 142 | if isinstance(self.sub, Binary) 143 | else f"{self.sub.latex()}" 144 | ) 145 | 146 | def __str__(self) -> str: 147 | return self.connective + ( 148 | f"({self.sub})" if isinstance(self.sub, Binary) else f"{self.sub}" 149 | ) 150 | 151 | def __repr__(self) -> str: 152 | return f"{self.signature}[{repr(self.sub)}]" 153 | 154 | 155 | class Negation(Unary): 156 | signature = "Neg" 157 | connective = "¬" 158 | connective_latex = r"\neg" 159 | 160 | 161 | class Binary(Formula): 162 | subs: tuple[Formula, Formula] 163 | 164 | def __init__(self, sub1: Formula, sub2: Formula): 165 | self.subs = (sub1, sub2) 166 | 167 | def clone(self): 168 | clones = [sub.clone() for sub in self.subs] 169 | for clone in clones: 170 | clone.__dict__ = deepcopy(clone.__dict__) 171 | return self.__class__(*clones) 172 | 173 | @property 174 | def atoms(self): 175 | atoms = dict() 176 | for subfml in self.subs: 177 | for k, v in subfml.atoms.items(): 178 | atoms[k] = atoms.get(k, []) + v 179 | return atoms 180 | 181 | @property 182 | def free_terms(self): 183 | free_terms = [] 184 | for subfml in self.subs: 185 | free_terms += subfml.free_terms 186 | return free_terms 187 | 188 | def replace_term(self, replaced_term, replacing_term): 189 | # fml = copy(self) 190 | fml = self 191 | subs = [] 192 | for subfml in self.subs: 193 | subs.append(subfml.replace_term(replaced_term, replacing_term)) 194 | fml.subs = subs 195 | return fml 196 | 197 | def latex(self): 198 | return f" {self.connective_latex} ".join( 199 | map( 200 | lambda x: f"({x.latex()})" if isinstance(x, Binary) else f"{x.latex()}", 201 | self.subs, 202 | ) 203 | ) 204 | 205 | def __str__(self) -> str: 206 | # print(self.subs) 207 | # return f"{self.connective}".join(map(lambda x: f"({x})", self.subs)) 208 | return f"{self.connective}".join( 209 | map( 210 | lambda x: f"({x})" if isinstance(x, Binary) else f"{x}", 211 | self.subs, 212 | ) 213 | ) 214 | 215 | def __repr__(self) -> str: 216 | return "{}[{}]".format( 217 | self.signature, ", ".join(map(lambda x: repr(x), self.subs)) 218 | ) 219 | 220 | 221 | class Conjunction(Binary): 222 | signature = "Conj" 223 | connective = "∧" 224 | connective_latex = r"\land" 225 | 226 | 227 | class Disjunction(Binary): 228 | signature = "Disj" 229 | connective = "∨" 230 | connective_latex = r"\lor" 231 | 232 | 233 | class Conditional(Binary): 234 | signature = "Cond" 235 | connective = "→" 236 | connective_latex = r"\to" 237 | 238 | 239 | class Quantifier(Formula): 240 | def __init__(self, term, sub: Formula): 241 | self.variable = term 242 | self.sub = sub 243 | 244 | @property 245 | def atoms(self): 246 | return self.sub.atoms 247 | # atoms = [] 248 | # for atom in self.sub.atoms: 249 | # bounded_variables = atom.get("bounded_variables", []) + [self.variable] 250 | # atoms.append(dict(atom, bounded_variables=bounded_variables)) 251 | # return atoms 252 | 253 | @property 254 | def free_terms(self): 255 | return [term for term in self.sub.free_terms if term not in self.variable] 256 | 257 | def replace_term(self, replaced_term, replacing_term): 258 | return self.sub.replace_term(replaced_term, replacing_term) 259 | 260 | def clone(self): 261 | return Quantifier(copy(self.variable), self.sub.clone()) 262 | 263 | def latex(self): 264 | return f"{self.connective_latex} {self.variable} " + ( 265 | f"({self.sub.latex()})" 266 | if isinstance(self.sub, Binary) 267 | else f"{self.sub.latex()}" 268 | ) 269 | 270 | def __str__(self) -> str: 271 | return f"{self.connective}{self.variable}" + ( 272 | f"({self.sub})" if isinstance(self.sub, Binary) else f"{self.sub}" 273 | ) 274 | 275 | def __repr__(self) -> str: 276 | return f"{self.signature}<{self.variable}>[{repr(self.sub)}]" 277 | 278 | 279 | class Universal(Quantifier): 280 | signature = "Forall" 281 | connective = "∀" 282 | connective_latex = r"\forall" 283 | 284 | 285 | class Particular(Quantifier): 286 | signature = "Exists" 287 | connective = "∃" 288 | connective_latex = r"\exists" 289 | -------------------------------------------------------------------------------- /mathesis/grammars.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | 5 | from lark import Lark, Transformer 6 | 7 | from mathesis.forms import ( 8 | Atom, 9 | Top, 10 | Bottom, 11 | Conditional, 12 | Conjunction, 13 | Disjunction, 14 | Negation, 15 | Particular, 16 | Universal, 17 | ) 18 | 19 | 20 | class ToFml(Transformer): 21 | def atom(self, v): 22 | if len(v) == 1: # only a predicate (proposition) 23 | return Atom(v[0]) 24 | else: 25 | return Atom(v[0], terms=v[1:]) 26 | 27 | def top(self, v): 28 | return Top() 29 | 30 | def bottom(self, v): 31 | return Bottom() 32 | 33 | def negation(self, v): 34 | return Negation(*v) 35 | 36 | def universal(self, v): 37 | return Universal(*v) 38 | 39 | def particular(self, v): 40 | return Particular(*v) 41 | 42 | def conjunction(self, v): 43 | return Conjunction(*v) 44 | 45 | def disjunction(self, v): 46 | return Disjunction(*v) 47 | 48 | def conditional(self, v): 49 | return Conditional(*v) 50 | 51 | 52 | class Grammar(ABC): 53 | """Abstract class for grammars.""" 54 | 55 | grammar_rules: str 56 | 57 | def __repr__(self): 58 | return self.grammar_rules 59 | 60 | # @abstractmethod 61 | # def parse(self, text_or_list: str | list): 62 | # raise NotImplementedError() 63 | 64 | def __init__(self): 65 | self.grammar = Lark(self.grammar_rules, start="fml") 66 | 67 | def parse(self, text_or_list: str | list): 68 | """Parse a string or a list of strings into formula object(s). 69 | 70 | Args: 71 | text_or_list (str | list): A string or a list of strings representing formula(s). 72 | """ 73 | 74 | # print(fml_strings) 75 | if isinstance(text_or_list, list): 76 | fml_strings = text_or_list 77 | fmls = [] 78 | for fml_string in fml_strings: 79 | tree = self.grammar.parse(fml_string) 80 | fml = ToFml().transform(tree) 81 | fmls.append(fml) 82 | return fmls 83 | else: 84 | fml_string = text_or_list 85 | tree = self.grammar.parse(fml_string) 86 | fml = ToFml().transform(tree) 87 | return fml 88 | 89 | 90 | class BasicPropositionalGrammar(Grammar): 91 | """Basic grammar for the propositional language.""" 92 | 93 | grammar_rules = r""" 94 | ?fml: conditional 95 | | disjunction 96 | | conjunction 97 | | negation 98 | | top 99 | | bottom 100 | | atom 101 | | "(" fml ")" 102 | 103 | ATOM : /\w+/ 104 | 105 | atom : ATOM 106 | top : "⊤" 107 | bottom : "⊥" 108 | negation : "¬" fml 109 | conjunction : (conjunction | fml) "∧" fml 110 | disjunction : (disjunction | fml) "∨" fml 111 | conditional : fml "{conditional_symbol}" fml 112 | necc : "□" fml 113 | poss : "◇" fml 114 | 115 | %import common.WS 116 | %ignore WS 117 | """.lstrip() 118 | 119 | def __init__(self, symbols={"conditional": "→"}): 120 | self.grammar_rules = self.grammar_rules.format( 121 | conditional_symbol=symbols["conditional"] 122 | ) 123 | super().__init__() 124 | 125 | 126 | class BasicGrammar(BasicPropositionalGrammar): 127 | """Basic grammar for the first-order language.""" 128 | 129 | grammar_rules = r""" 130 | ?fml: conditional 131 | | disjunction 132 | | conjunction 133 | | negation 134 | | universal 135 | | particular 136 | | top 137 | | bottom 138 | | atom 139 | | "(" fml ")" 140 | 141 | PREDICATE: /\w+/ 142 | TERM: /\w+/ 143 | 144 | atom : PREDICATE ("(" TERM ("," TERM)* ")")? 145 | top : "⊤" 146 | bottom : "⊥" 147 | negation : "¬" fml 148 | conjunction : fml "∧" fml 149 | disjunction : fml "∨" fml 150 | conditional : fml "{conditional_symbol}" fml 151 | necc : "□" fml 152 | poss : "◇" fml 153 | universal : "∀" TERM fml 154 | particular : "∃" TERM fml 155 | 156 | %import common.WS 157 | %ignore WS 158 | """.lstrip() 159 | -------------------------------------------------------------------------------- /mathesis/semantics/model.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from itertools import permutations 4 | from typing import Any, Callable 5 | 6 | from mathesis import forms 7 | from mathesis.semantics.truth_table import classical as truth_table 8 | 9 | 10 | def _normalize_predicates(predicates: dict[str, set[Any]]): 11 | return dict( 12 | map( 13 | lambda x: ( 14 | (x[0], tuple((v,) for v in x[1])) 15 | if (len(x[1]) > 0 and type(list(x[1])[0]) is not tuple) 16 | else x 17 | ), 18 | predicates.items(), 19 | ) 20 | ) 21 | 22 | 23 | class Model: 24 | """ 25 | A set-theoretic model. 26 | """ 27 | 28 | def __init__( 29 | self, 30 | domain: set[Any] = set(), 31 | predicates: dict[str, set[Any]] = dict(), 32 | constants: dict[str, Any] = dict(), 33 | functions: dict[str, Callable] = dict(), 34 | ): 35 | """ 36 | Args: 37 | domain: A set of objects. 38 | predicates: A dictionary with predicate symbols as keys and sets of tuples of objects as values, 39 | or a function that assigns sets of tuples of objects to predicate symbols. 40 | constants: A dictionary with constant symbols as keys and objects as values, 41 | or a function that assigns objects to constants. 42 | functions: A dictionary with function symbols as keys and functions over domain as values. 43 | """ 44 | self.domain = domain 45 | # Make sure that the value of a predicate is a list of tuples 46 | predicates = _normalize_predicates(predicates) 47 | self.predicates = predicates 48 | self.constants = constants 49 | self.functions = functions 50 | 51 | # def assign_term_denotation(self, term): 52 | # if term in self.variables: 53 | # pass 54 | # elif term in self.constants: 55 | # return self.denotations.get(term, term) 56 | 57 | def valuate(self, fml: forms.Formula, variable_assignment: dict[str, Any] = dict()): 58 | """ 59 | Valuate a formula in a model. 60 | 61 | Args: 62 | fml: A formula. 63 | variable_assignment: A dictionary with variable symbols as keys and assigned objects as values 64 | """ 65 | if isinstance(fml, forms.Top): 66 | return 1 67 | 68 | elif isinstance(fml, forms.Bottom): 69 | return 0 70 | 71 | elif isinstance(fml, forms.Atom): 72 | # Denotations of the terms, a list to be converted to a tuple 73 | term_denotations = [] 74 | 75 | # Iterate over all terms in the formula 76 | for term in fml.terms: 77 | if term in self.constants: 78 | denotation = self.constants.get(term) 79 | 80 | elif term in variable_assignment: 81 | denotation = variable_assignment[term] 82 | 83 | # Fallback: use the term itself as denotation 84 | elif term in self.domain: 85 | denotation = term 86 | 87 | else: 88 | raise RuntimeError(f"Variable assignment not given: '{term}'") 89 | 90 | term_denotations.append(denotation) 91 | 92 | term_denotations = tuple(term_denotations) 93 | 94 | # Obtain the extension of the predicate 95 | if fml.predicate in self.predicates: 96 | extension = self.predicates.get(fml.predicate) 97 | else: 98 | raise Exception("Undefined predicate") 99 | # print(term_denotations, extension) 100 | 101 | if term_denotations in extension: 102 | return 1 103 | else: 104 | return 0 105 | 106 | elif isinstance(fml, forms.Universal): 107 | # print(fml.sub, fml.variable, fml.sub.free_terms) 108 | values = [] 109 | 110 | for obj in self.domain: 111 | # print(fml.variable, obj) 112 | value = self.valuate( 113 | fml.sub, 114 | variable_assignment=dict( 115 | variable_assignment, **{fml.variable: obj} 116 | ), 117 | ) 118 | values.append(value) 119 | 120 | # Return true if true in all possible assignments 121 | if all(value == 1 for value in values): 122 | return 1 123 | else: 124 | return 0 125 | 126 | elif isinstance(fml, forms.Particular): 127 | values = [] 128 | 129 | for obj in self.domain: 130 | value = self.valuate( 131 | fml.sub, 132 | variable_assignment=dict( 133 | variable_assignment, **{fml.variable: obj} 134 | ), 135 | ) 136 | values.append(value) 137 | 138 | # Return true if true in some possible assignments 139 | if any(value == 1 for value in values): 140 | return 1 141 | else: 142 | return 0 143 | 144 | elif isinstance(fml, forms.Negation): 145 | return truth_table.NegationClause().apply( 146 | self.valuate(fml.sub, variable_assignment=variable_assignment) 147 | ) 148 | 149 | elif isinstance(fml, forms.Conjunction): 150 | return truth_table.ConjunctionClause().apply( 151 | *tuple( 152 | self.valuate(child, variable_assignment=variable_assignment) 153 | for child in fml.subs 154 | ) 155 | ) 156 | 157 | elif isinstance(fml, forms.Disjunction): 158 | return truth_table.DisjunctionClause().apply( 159 | *tuple( 160 | self.valuate(child, variable_assignment=variable_assignment) 161 | for child in fml.subs 162 | ) 163 | ) 164 | 165 | elif isinstance(fml, forms.Conditional): 166 | return truth_table.ConditionalClause().apply( 167 | *tuple( 168 | self.valuate(child, variable_assignment=variable_assignment) 169 | for child in fml.subs 170 | ) 171 | ) 172 | 173 | def validates( 174 | self, premises: list[forms.Formula] = [], conclusions: list[forms.Formula] = [] 175 | ): 176 | """ 177 | Return true if the model validates the inference given premises and conclusions. 178 | 179 | Args: 180 | premises: A list of premise formulas. 181 | conclusions: A list of conclusion formulas. 182 | """ 183 | 184 | # List up all free variables in premises and conclusions 185 | free_variables = set() 186 | for fml in premises + conclusions: 187 | free_variables.update(fml.free_terms) 188 | free_variables -= self.constants.keys() 189 | # print(free_variables) 190 | 191 | # List up all possible variable assignments 192 | variable_assignments = [] 193 | for sequence_of_objects in permutations(self.domain, len(free_variables)): 194 | # print("sequence_of_objects", sequence_of_objects) 195 | variable_assignment = dict(zip(free_variables, sequence_of_objects)) 196 | variable_assignments.append(variable_assignment) 197 | # print("variable_assignments", variable_assignments) 198 | 199 | is_valid_with_assignment = [] 200 | 201 | for variable_assignment in variable_assignments: 202 | premise_values = [] 203 | conclusion_values = [] 204 | 205 | for fml in premises: 206 | value = self.valuate(fml, variable_assignment=variable_assignment) 207 | premise_values.append(value) 208 | 209 | for fml in conclusions: 210 | value = self.valuate(fml, variable_assignment=variable_assignment) 211 | conclusion_values.append(value) 212 | 213 | if all(value == 1 for value in premise_values) and any( 214 | value != 1 for value in conclusion_values 215 | ): 216 | is_valid_with_assignment.append(False) 217 | else: 218 | is_valid_with_assignment.append(True) 219 | 220 | if all(is_valid_with_assignment): 221 | return True 222 | else: 223 | return False 224 | 225 | 226 | # # TODO: Multiple kinds of accessibility relations 227 | # class Frame: 228 | # def __init__( 229 | # self, 230 | # states: List[Model] = [], 231 | # accessibility_relations: set[Tuple[Any, Any]] = set(), 232 | # ): 233 | # self.states = states 234 | # self.accessibility_relations = accessibility_relations 235 | -------------------------------------------------------------------------------- /mathesis/semantics/truth_table/__init__.py: -------------------------------------------------------------------------------- 1 | from mathesis.semantics.truth_table.base import * 2 | from mathesis.semantics.truth_table.classical import ClassicalTruthTable 3 | from mathesis.semantics.truth_table.lp import LPTruthTable 4 | from mathesis.semantics.truth_table.k3 import K3TruthTable 5 | from mathesis.semantics.truth_table.l3 import L3TruthTable 6 | from mathesis.semantics.truth_table.fde import FDETruthTable 7 | -------------------------------------------------------------------------------- /mathesis/semantics/truth_table/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from itertools import product 5 | 6 | from anytree import NodeMixin, PostOrderIter 7 | from prettytable import PLAIN_COLUMNS, PrettyTable 8 | 9 | from mathesis import forms 10 | 11 | _logger = logging.getLogger(__name__) 12 | _logger.addHandler(logging.NullHandler()) 13 | 14 | 15 | class ConnectiveClause: 16 | column_names: list[str] 17 | table: dict 18 | truth_value_symbols: None 19 | 20 | def __init__(self, truth_value_symbols=None) -> None: 21 | self.truth_value_symbols = truth_value_symbols 22 | 23 | def __str__(self): 24 | return self.to_string() 25 | 26 | def apply(self, *values): 27 | if None in values: 28 | return None 29 | return self.table[values] 30 | 31 | def to_table(self): 32 | table = PrettyTable() 33 | table.align = "c" 34 | table.format = True 35 | 36 | places = len(list(self.table.keys())[0]) 37 | 38 | table.field_names = self.column_names 39 | 40 | for values_from, value_to in self.table.items(): 41 | if self.truth_value_symbols: 42 | values_from = tuple( 43 | self.truth_value_symbols.get(v, v) for v in values_from 44 | ) 45 | value_to = self.truth_value_symbols.get(value_to, value_to) 46 | table.add_row(values_from + (value_to,)) 47 | 48 | return table 49 | 50 | def to_string(self, style=PLAIN_COLUMNS): 51 | table = self.to_table() 52 | table.set_style(style) 53 | return str(table) 54 | 55 | def _repr_html_(self): 56 | table = self.to_table() 57 | return table.get_html_string(format=False) 58 | 59 | 60 | class AssignedNodeBase: 61 | pass 62 | 63 | 64 | class AssignedNode(AssignedNodeBase, NodeMixin): 65 | def __init__(self, name, fml, parent=None, children=[]): 66 | self.name = name 67 | self.fml = fml 68 | self.parent = parent 69 | self.children = children 70 | self._truth_value = None 71 | 72 | @property 73 | def truth_value(self): 74 | # NOTE: Make sure to specify `is not None`: _truth_value can be 0 and accidentally return False 75 | if self._truth_value is not None: 76 | return self._truth_value 77 | else: 78 | raise Exception("Truth value not assigned") 79 | 80 | @truth_value.setter 81 | def truth_value(self, value): 82 | self._truth_value = value 83 | 84 | def assign_atom_values(self, atom_assignments): 85 | if isinstance(self.fml, forms.Atom): 86 | self._truth_value = atom_assignments.get(str(self.fml), None) 87 | else: 88 | for child in self.children: 89 | child.assign_atom_values(atom_assignments) 90 | 91 | def __repr__(self): 92 | return f"{repr(self.fml)} = {self.truth_value}" 93 | 94 | def __str__(self): 95 | return f"{str(self.fml)} = {self.truth_value}" 96 | 97 | 98 | class TruthTable: 99 | """Base class for truth tables. 100 | 101 | Args: 102 | formula_or_formulas: A single formula or a list of formulas. 103 | conclusions: A list of formulas. 104 | """ 105 | 106 | truth_values: set = set() 107 | designated_values: set = set() 108 | clauses = {} 109 | 110 | def __init__( 111 | self, 112 | formula_or_formulas: list[forms.Formula] | forms.Formula, 113 | conclusions: list[forms.Formula] = [], # TODO: Distinction between [] and None 114 | ): 115 | if isinstance(formula_or_formulas, list): 116 | # raise NotImplementedError() 117 | self.premises = formula_or_formulas 118 | else: 119 | self.premises = [formula_or_formulas] 120 | 121 | self.conclusions = conclusions 122 | 123 | def __str__(self): 124 | return self.to_string() 125 | 126 | @property 127 | def atom_columns(self): 128 | atom_symbols = self.atom_symbols 129 | return atom_symbols 130 | 131 | @property 132 | def atom_symbols(self): 133 | atom_symbols = set() 134 | # Extract all atomic symbols in the formulas 135 | for fml in self.premises + self.conclusions: 136 | atom_symbols.update(fml.atoms.keys()) 137 | atom_symbols = sorted(tuple(atom_symbols)) 138 | return atom_symbols 139 | 140 | def assignments(self): 141 | """Generate all possible truth assignments for the atomic symbols in the formulas.""" 142 | 143 | atom_symbols = self.atom_symbols 144 | 145 | assignments = [] 146 | for tv in product(self.truth_values, repeat=len(atom_symbols)): 147 | assignment = dict(zip(atom_symbols, tv)) 148 | assignments.append(assignment) 149 | 150 | return assignments 151 | 152 | @property 153 | def columns(self): 154 | atom_columns = [self.atom_columns] 155 | nonatom_columns = [] 156 | 157 | for fml in self.premises + self.conclusions: 158 | tree = self._wrap_fml(fml) 159 | fml_columns = [] 160 | 161 | for node in PostOrderIter(tree): 162 | if str(node.fml) not in map(str, fml_columns): 163 | if isinstance(node.fml, forms.Atom): 164 | pass 165 | else: 166 | fml_columns.append(node.fml) 167 | 168 | nonatom_columns.append(fml_columns) 169 | 170 | columns = atom_columns + nonatom_columns 171 | 172 | # TODO: Allow flexible stringification 173 | return [tuple(map(str, column)) for column in columns] 174 | 175 | @property 176 | def rows(self): 177 | rows = [] 178 | 179 | for assignment in self.assignments(): 180 | row = [] 181 | atom_tvs = [value for value in assignment.values()] 182 | atom_segment = [] 183 | for value in atom_tvs: 184 | if hasattr(self, "truth_value_symbols"): 185 | atom_segment.append( 186 | getattr(self, "truth_value_symbols").get(value, value) 187 | ) 188 | else: 189 | atom_segment.append(value) 190 | 191 | row.append(tuple(atom_segment)) 192 | 193 | for fml in self.premises + self.conclusions: 194 | tree = self._wrap_fml(fml) 195 | tree.assign_atom_values(assignment) 196 | atom_value_pairs = [] 197 | nonatom_value_pairs = [] 198 | field_names = [] 199 | 200 | for node in PostOrderIter(tree): 201 | self.compute_truth_value(node) 202 | # print(str(node.fml), node.truth_value) 203 | if str(node.fml) not in field_names: 204 | field_names.append(str(node.fml)) 205 | if hasattr(self, "truth_value_symbols"): 206 | truth_value = getattr(self, "truth_value_symbols").get( 207 | node.truth_value, node.truth_value 208 | ) 209 | else: 210 | truth_value = node.truth_value 211 | if isinstance(node.fml, forms.Atom): 212 | atom_value_pairs.append((node.fml, truth_value)) 213 | else: 214 | nonatom_value_pairs.append((node.fml, truth_value)) 215 | # row.append(truth_value) 216 | 217 | # print(atom_value_pairs, nonatom_value_pairs) 218 | # field_names = [str(fml) for fml, value in atom_value_pairs + nonatom_value_pairs] 219 | row_segment = tuple(value for fml, value in nonatom_value_pairs) 220 | row.append(row_segment) 221 | 222 | rows.append(row) 223 | 224 | return rows 225 | 226 | # formula_row_segments.append(nonatom_value_pairs) 227 | 228 | def compute_truth_value(self, assigned_node): 229 | if isinstance(assigned_node.fml, forms.Top): 230 | assigned_node.truth_value = 1 231 | elif isinstance(assigned_node.fml, forms.Bottom): 232 | assigned_node.truth_value = 0 233 | elif isinstance(assigned_node.fml, forms.Atom): 234 | # assigned_node.truth_value = assigned_node._truth_value 235 | pass 236 | elif isinstance(assigned_node.fml, forms.Negation): 237 | if forms.Negation not in self.clauses: 238 | raise NotImplementedError("Negation clause not implemented") 239 | assigned_node.truth_value = self.clauses[forms.Negation].apply( 240 | assigned_node.children[0].truth_value 241 | ) 242 | elif isinstance(assigned_node.fml, forms.Conjunction): 243 | if forms.Conjunction not in self.clauses: 244 | raise NotImplementedError("Conjunction clause not implemented") 245 | assigned_node.truth_value = self.clauses[forms.Conjunction].apply( 246 | *tuple(child.truth_value for child in assigned_node.children) 247 | ) 248 | elif isinstance(assigned_node.fml, forms.Disjunction): 249 | if forms.Disjunction not in self.clauses: 250 | raise NotImplementedError("Disjunction clause not implemented") 251 | assigned_node.truth_value = self.clauses[forms.Disjunction].apply( 252 | *tuple(child.truth_value for child in assigned_node.children) 253 | ) 254 | elif isinstance(assigned_node.fml, forms.Conditional): 255 | if forms.Conditional not in self.clauses: 256 | raise NotImplementedError("Conditional clause not implemented") 257 | assigned_node.truth_value = self.clauses[forms.Conditional].apply( 258 | *tuple(child.truth_value for child in assigned_node.children) 259 | ) 260 | else: 261 | raise NotImplementedError( 262 | f"Clause for {type(assigned_node.fml)} not implemented" 263 | ) 264 | 265 | def _wrap_fml(self, fml): 266 | def transformer(fml): 267 | node = AssignedNode(str(fml), fml=fml) 268 | if isinstance(fml, forms.Binary): 269 | node.children = [transformer(subfml) for subfml in fml.subs] 270 | elif isinstance(fml, forms.Unary): 271 | node.children = [transformer(fml.sub)] 272 | # NOTE: No truth table for quantifiers 273 | # elif isinstance(fml, forms.Quantifier): 274 | # node.children = [transformer(fml.sub)] 275 | return node 276 | 277 | wrapped_fml = fml.transform(transformer) 278 | return wrapped_fml 279 | 280 | def is_valid(self): 281 | atom_symbols = set() 282 | # Extract all atomic symbols in the formulas 283 | for fml in self.premises + self.conclusions: 284 | atom_symbols.update(fml.atoms.keys()) 285 | atom_symbols = sorted(list(atom_symbols)) 286 | 287 | premise_values = [] 288 | conclusion_values = [] 289 | 290 | for tv in product(self.truth_values, repeat=len(atom_symbols)): 291 | for fml in self.premises: 292 | assignments = dict(zip(atom_symbols, tv)) 293 | tv_fml = self._wrap_fml(fml) 294 | tv_fml.assign_atom_values(assignments) 295 | for node in PostOrderIter(tv_fml): 296 | self.compute_truth_value(node) 297 | premise_values.append(tv_fml.truth_value) 298 | 299 | for fml in self.conclusions: 300 | assignments = dict(zip(atom_symbols, tv)) 301 | tv_fml = self._wrap_fml(fml) 302 | tv_fml.assign_atom_values(assignments) 303 | for node in PostOrderIter(tv_fml): 304 | self.compute_truth_value(node) 305 | conclusion_values.append(tv_fml.truth_value) 306 | 307 | # print(premise_values) 308 | 309 | if not self.conclusions: 310 | if all([value in self.designated_values for value in premise_values]): 311 | return True 312 | else: 313 | return False 314 | else: 315 | # TODO: Implement for inference validity 316 | raise NotImplementedError() 317 | 318 | # def is_valid(self): 319 | # for fml in self.premises + self.conclusions: 320 | # atom_symbols = fml.atoms.keys() 321 | # atom_symbols = sorted(atom_symbols) 322 | # values = [] 323 | # for tv in product(self.truth_values, repeat=len(atom_symbols)): 324 | # assignments = dict(zip(atom_symbols, tv)) 325 | # tv_fml = self._wrap_fml(fml) 326 | # tv_fml.assign_atom_values(assignments) 327 | # for node in PostOrderIter(tv_fml): 328 | # self.compute_truth_value(node) 329 | # values.append(tv_fml.truth_value) 330 | # if all([value in self.designated_values for value in values]): 331 | # return True 332 | # else: 333 | # return False 334 | 335 | def counterexample(self): 336 | pass 337 | 338 | def to_table(self): 339 | table = PrettyTable() 340 | table.align = "c" 341 | table.format = True 342 | 343 | for assignment in self.assignments(): 344 | formula_row_segments = [] 345 | for fml in self.premises + self.conclusions: 346 | tree = self._wrap_fml(fml) 347 | # print(RenderTree(tree)) 348 | tree.assign_atom_values(assignment) 349 | # print(tree.truth_value) 350 | # print(RenderTree(tree)) 351 | atom_value_pairs = [] 352 | nonatom_value_pairs = [] 353 | field_names = [] 354 | row = [] 355 | 356 | for node in PostOrderIter(tree): 357 | self.compute_truth_value(node) 358 | # print(str(node.fml), node.truth_value) 359 | if str(node.fml) not in field_names: 360 | field_names.append(str(node.fml)) 361 | if hasattr(self, "truth_value_symbols"): 362 | truth_value = getattr(self, "truth_value_symbols").get( 363 | node.truth_value, node.truth_value 364 | ) 365 | else: 366 | truth_value = node.truth_value 367 | if isinstance(node.fml, forms.Atom): 368 | atom_value_pairs.append((node.fml, truth_value)) 369 | else: 370 | nonatom_value_pairs.append((node.fml, truth_value)) 371 | # row.append(truth_value) 372 | 373 | # print(atom_value_pairs, nonatom_value_pairs) 374 | field_names = [str(fml) for fml, value in nonatom_value_pairs] 375 | row = [value for fml, value in nonatom_value_pairs] 376 | # print(field_names, row) 377 | 378 | formula_row_segments.append(nonatom_value_pairs) 379 | 380 | # print(assignment) 381 | # print(formula_row_segments) 382 | 383 | field_names = [] 384 | row = [] 385 | 386 | for atom, value in assignment.items(): 387 | truth_value = getattr(self, "truth_value_symbols", {}).get(value, value) 388 | field_names.append(str(atom)) 389 | row.append(truth_value) 390 | 391 | i = 0 392 | for nonatom_value_pairs in formula_row_segments: 393 | # TODO: Fix hacky way to satisfy PrettyTable's unique field_names requirement 394 | field_names += [ 395 | str(fml) + " " * i for fml, value in nonatom_value_pairs 396 | ] 397 | row += [value for fml, value in nonatom_value_pairs] 398 | i += 1 399 | 400 | if not table.field_names: 401 | # Create field_names from fml of each nonatom_value_pairs of formula_row_segments 402 | table.field_names = field_names 403 | 404 | table.add_row(row) 405 | 406 | return table 407 | 408 | def to_string(self, style=PLAIN_COLUMNS): 409 | table = self.to_table() 410 | table.set_style(style) 411 | return str(table) 412 | 413 | def _repr_html_(self): 414 | table = self.to_table() 415 | return table.get_html_string(format=False) 416 | -------------------------------------------------------------------------------- /mathesis/semantics/truth_table/classical.py: -------------------------------------------------------------------------------- 1 | from mathesis import forms 2 | from mathesis.semantics.truth_table.base import TruthTable, ConnectiveClause 3 | 4 | 5 | class NegationClause(ConnectiveClause): 6 | column_names = ["P", "Negation(P)"] 7 | # TODO: Custom truth values 8 | table = { 9 | (1,): 0, 10 | (0,): 1, 11 | } 12 | 13 | 14 | class ConjunctionClause(ConnectiveClause): 15 | column_names = ["P", "Q", "Conjunction(P, Q)"] 16 | table = { 17 | (1, 1): 1, 18 | (1, 0): 0, 19 | (0, 1): 0, 20 | (0, 0): 0, 21 | } 22 | 23 | 24 | class DisjunctionClause(ConnectiveClause): 25 | column_names = ["P", "Q", "Disjunction(P, Q)"] 26 | table = { 27 | (1, 1): 1, 28 | (1, 0): 1, 29 | (0, 1): 1, 30 | (0, 0): 0, 31 | } 32 | 33 | 34 | class ConditionalClause(ConnectiveClause): 35 | column_names = ["P", "Q", "Conditional(P, Q)"] 36 | table = { 37 | (1, 1): 1, 38 | (1, 0): 0, 39 | (0, 1): 1, 40 | (0, 0): 1, 41 | } 42 | 43 | 44 | class ClassicalTruthTable(TruthTable): 45 | """The classical truth table class.""" 46 | 47 | truth_values = {1, 0} 48 | designated_values = {1} 49 | truth_value_symbols = {1: "1", 0: "0"} 50 | clauses = { 51 | forms.Negation: NegationClause(), 52 | forms.Conjunction: ConjunctionClause(), 53 | forms.Disjunction: DisjunctionClause(), 54 | forms.Conditional: ConditionalClause(), 55 | } 56 | -------------------------------------------------------------------------------- /mathesis/semantics/truth_table/fde.py: -------------------------------------------------------------------------------- 1 | from mathesis import forms 2 | from mathesis.semantics.truth_table.base import TruthTable, ConnectiveClause 3 | 4 | 5 | class NegationClause(ConnectiveClause): 6 | column_names = ["P", "Negation(P)"] 7 | # TODO: Custom truth values 8 | table = { 9 | (1,): 0, 10 | (0,): 1, 11 | (2,): 2, 12 | (0.5,): 0.5, 13 | } 14 | 15 | 16 | class ConjunctionClause(ConnectiveClause): 17 | column_names = ["P", "Q", "Conjunction(P, Q)"] 18 | table = { 19 | (1, 1): 1, 20 | (1, 2): 2, 21 | (1, 0): 0, 22 | (1, 0.5): 0.5, 23 | (2, 1): 2, 24 | (2, 2): 2, 25 | (2, 0): 0, 26 | (2, 0.5): 0.5, 27 | (0, 1): 0, 28 | (0, 2): 0, 29 | (0, 0): 0, 30 | (0, 0.5): 0, 31 | (0.5, 1): 0.5, 32 | (0.5, 2): 0.5, 33 | (0.5, 0): 0, 34 | (0.5, 0.5): 0.5, 35 | } 36 | 37 | 38 | class DisjunctionClause(ConnectiveClause): 39 | column_names = ["P", "Q", "Disjunction(P, Q)"] 40 | table = { 41 | (1, 1): 1, 42 | (1, 2): 1, 43 | (1, 0): 1, 44 | (1, 0.5): 1, 45 | (2, 1): 1, 46 | (2, 2): 2, 47 | (2, 0): 2, 48 | (2, 0.5): 2, 49 | (0, 1): 1, 50 | (0, 2): 2, 51 | (0, 0): 0, 52 | (0, 0.5): 0.5, 53 | (0.5, 1): 1, 54 | (0.5, 2): 2, 55 | (0.5, 0): 0.5, 56 | (0.5, 0.5): 0.5, 57 | } 58 | 59 | 60 | class ConditionalClause(ConnectiveClause): 61 | column_names = ["P", "Q", "Conditional(P, Q)"] 62 | table = { 63 | (1, 1): 1, 64 | (1, 2): 2, 65 | (1, 0): 0, 66 | (1, 0.5): 0.5, 67 | (2, 1): 1, 68 | (2, 2): 2, 69 | (2, 0): 2, 70 | (2, 0.5): 2, 71 | (0, 1): 1, 72 | (0, 2): 1, 73 | (0, 0): 1, 74 | (0, 0.5): 1, 75 | (0.5, 1): 1, 76 | (0.5, 2): 2, 77 | (0.5, 0): 0.5, 78 | (0.5, 0.5): 0.5, 79 | } 80 | 81 | 82 | class FDETruthTable(TruthTable): 83 | """The 4-valued logic FDE truth table class.""" 84 | 85 | truth_values = {0, 1, 0.5, 2} 86 | designated_values = {1, 2} 87 | truth_value_symbols = {1: "1", 0: "0", 2: "b", 0.5: "i"} 88 | clauses = { 89 | forms.Negation: NegationClause(), 90 | forms.Conjunction: ConjunctionClause(), 91 | forms.Disjunction: DisjunctionClause(), 92 | forms.Conditional: ConditionalClause(), 93 | } 94 | -------------------------------------------------------------------------------- /mathesis/semantics/truth_table/k3.py: -------------------------------------------------------------------------------- 1 | from mathesis import forms 2 | from mathesis.semantics.truth_table.base import TruthTable, ConnectiveClause 3 | 4 | 5 | class NegationClause(ConnectiveClause): 6 | column_names = ["P", "Negation(P)"] 7 | # TODO: Custom truth values 8 | table = { 9 | (1,): 0, 10 | (0.5,): 0.5, 11 | (0,): 1, 12 | } 13 | 14 | 15 | class ConjunctionClause(ConnectiveClause): 16 | column_names = ["P", "Q", "Conjunction(P, Q)"] 17 | table = { 18 | (1, 1): 1, 19 | (1, 0.5): 0.5, 20 | (1, 0): 0, 21 | (0.5, 1): 0.5, 22 | (0.5, 0.5): 0.5, 23 | (0.5, 0): 0, 24 | (0, 1): 0, 25 | (0, 0.5): 0, 26 | (0, 0): 0, 27 | } 28 | 29 | 30 | class DisjunctionClause(ConnectiveClause): 31 | column_names = ["P", "Q", "Disjunction(P, Q)"] 32 | table = { 33 | (1, 1): 1, 34 | (1, 0.5): 1, 35 | (1, 0): 1, 36 | (0.5, 1): 1, 37 | (0.5, 0.5): 0.5, 38 | (0.5, 0): 0.5, 39 | (0, 1): 1, 40 | (0, 0.5): 0.5, 41 | (0, 0): 0, 42 | } 43 | 44 | 45 | class ConditionalClause(ConnectiveClause): 46 | column_names = ["P", "Q", "Conditional(P, Q)"] 47 | table = { 48 | (1, 1): 1, 49 | (1, 0.5): 0.5, 50 | (1, 0): 0, 51 | (0.5, 1): 1, 52 | (0.5, 0.5): 0.5, 53 | (0.5, 0): 0.5, 54 | (0, 1): 1, 55 | (0, 0.5): 1, 56 | (0, 0): 1, 57 | } 58 | 59 | 60 | class K3TruthTable(TruthTable): 61 | """The 3-valued logic K3 truth table class.""" 62 | 63 | truth_values = {1, 0, 0.5} 64 | designated_values = {1} 65 | truth_value_symbols = {1: "1", 0: "0", 0.5: "i"} 66 | clauses = { 67 | forms.Negation: NegationClause(), 68 | forms.Conjunction: ConjunctionClause(), 69 | forms.Disjunction: DisjunctionClause(), 70 | forms.Conditional: ConditionalClause(), 71 | } 72 | -------------------------------------------------------------------------------- /mathesis/semantics/truth_table/l3.py: -------------------------------------------------------------------------------- 1 | from mathesis import forms 2 | from mathesis.semantics.truth_table.base import TruthTable, ConnectiveClause 3 | from mathesis.semantics.truth_table.k3 import ( 4 | NegationClause, 5 | DisjunctionClause, 6 | ConjunctionClause, 7 | ) 8 | 9 | 10 | class ConditionalClause(ConnectiveClause): 11 | column_names = ["P", "Q", "Conditional(P, Q)"] 12 | table = { 13 | (1, 1): 1, 14 | (1, 0.5): 0.5, 15 | (1, 0): 0, 16 | (0.5, 1): 1, 17 | (0.5, 0.5): 1, 18 | (0.5, 0): 0.5, 19 | (0, 1): 1, 20 | (0, 0.5): 1, 21 | (0, 0): 1, 22 | } 23 | 24 | 25 | class L3TruthTable(TruthTable): 26 | """The 3-valued logic Ł3 truth table class.""" 27 | 28 | truth_values = {1, 0, 0.5} 29 | designated_values = {1} 30 | truth_value_symbols = {1: "1", 0: "0", 0.5: "i"} 31 | clauses = { 32 | forms.Negation: NegationClause(), 33 | forms.Conjunction: ConjunctionClause(), 34 | forms.Disjunction: DisjunctionClause(), 35 | forms.Conditional: ConditionalClause(), 36 | } 37 | -------------------------------------------------------------------------------- /mathesis/semantics/truth_table/lp.py: -------------------------------------------------------------------------------- 1 | from mathesis import forms 2 | from mathesis.semantics.truth_table.base import TruthTable, ConnectiveClause 3 | 4 | 5 | class NegationClause(ConnectiveClause): 6 | column_names = ["P", "Negation(P)"] 7 | # TODO: Custom truth values 8 | table = { 9 | (1,): 0, 10 | (2,): 2, 11 | (0,): 1, 12 | } 13 | 14 | 15 | class ConjunctionClause(ConnectiveClause): 16 | column_names = ["P", "Q", "Conjunction(P, Q)"] 17 | table = { 18 | (1, 1): 1, 19 | (1, 2): 2, 20 | (1, 0): 0, 21 | (2, 1): 2, 22 | (2, 2): 2, 23 | (2, 0): 0, 24 | (0, 1): 0, 25 | (0, 2): 0, 26 | (0, 0): 0, 27 | } 28 | 29 | 30 | class DisjunctionClause(ConnectiveClause): 31 | column_names = ["P", "Q", "Disjunction(P, Q)"] 32 | table = { 33 | (1, 1): 1, 34 | (1, 2): 1, 35 | (1, 0): 1, 36 | (2, 1): 1, 37 | (2, 2): 2, 38 | (2, 0): 2, 39 | (0, 1): 1, 40 | (0, 2): 2, 41 | (0, 0): 0, 42 | } 43 | 44 | 45 | class ConditionalClause(ConnectiveClause): 46 | column_names = ["P", "Q", "Conditional(P, Q)"] 47 | table = { 48 | (1, 1): 1, 49 | (1, 2): 2, 50 | (1, 0): 0, 51 | (2, 1): 1, 52 | (2, 2): 2, 53 | (2, 0): 2, 54 | (0, 1): 1, 55 | (0, 2): 1, 56 | (0, 0): 1, 57 | } 58 | 59 | 60 | class LPTruthTable(TruthTable): 61 | """The 3-valued logic LP truth table class.""" 62 | 63 | truth_values = {1, 0, 2} 64 | designated_values = {1, 2} 65 | truth_value_symbols = {1: "1", 0: "0", 2: "i"} 66 | clauses = { 67 | forms.Negation: NegationClause(), 68 | forms.Conjunction: ConjunctionClause(), 69 | forms.Disjunction: DisjunctionClause(), 70 | forms.Conditional: ConditionalClause(), 71 | } 72 | -------------------------------------------------------------------------------- /mathesis/solvers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections import namedtuple 3 | from typing import List 4 | 5 | from mathesis import forms 6 | from mathesis.deduction.tableau import Tableau, rules 7 | 8 | logger = logging.getLogger(__name__) 9 | logger.addHandler(logging.NullHandler()) 10 | 11 | 12 | class Result: 13 | def __init__(self, tab): 14 | self.tab = tab 15 | 16 | def htree(self): 17 | return self.tab.tree() 18 | 19 | def is_valid(self): 20 | return self.tab.is_closed() 21 | 22 | 23 | class ClassicalSolver: 24 | """A simple solver for classical propositional logic.""" 25 | 26 | def solve(self, premises: List[forms.Formula], concusions: List[forms.Formula]): 27 | queue = [] 28 | tab = Tableau(premises, concusions) 29 | root = tab.root 30 | queue += [root] + list(root.descendants) 31 | while queue: 32 | logger.debug("queue: %s", queue) 33 | 34 | item = queue.pop(0) 35 | 36 | Tactic = namedtuple("Tactic", ["condition", "rule"]) 37 | 38 | # TODO: Use structural pattern matching with Python 3.10 39 | tactics = [ 40 | Tactic( 41 | lambda item: isinstance(item.fml, forms.Negation) 42 | and isinstance(item.fml.sub, forms.Negation), 43 | rules.DoubleNegationRule(), 44 | ), 45 | Tactic( 46 | lambda item: isinstance(item.fml, forms.Conjunction), 47 | rules.ConjunctionRule(), 48 | ), 49 | Tactic( 50 | lambda item: isinstance(item.fml, forms.Negation) 51 | and isinstance(item.fml.sub, forms.Disjunction), 52 | rules.NegatedDisjunctionRule(), 53 | ), 54 | Tactic( 55 | lambda item: isinstance(item.fml, forms.Negation) 56 | and isinstance(item.fml.sub, forms.Conditional), 57 | rules.NegatedConditionalRule(), 58 | ), 59 | Tactic( 60 | lambda item: isinstance(item.fml, forms.Disjunction), 61 | rules.DisjunctionRule(), 62 | ), 63 | Tactic( 64 | lambda item: isinstance(item.fml, forms.Negation) 65 | and isinstance(item.fml.sub, forms.Conjunction), 66 | rules.NegatedConjunctionRule(), 67 | ), 68 | Tactic( 69 | lambda item: isinstance(item.fml, forms.Conditional), 70 | rules.ConditionalRule(), 71 | ), 72 | ] 73 | rule = next((t.rule for t in tactics if t.condition(item)), None) 74 | if rule: 75 | logger.debug("applying rule: %s", rule) 76 | queue_items = tab.apply_and_queue(item, rule) 77 | queue += queue_items 78 | logger.debug("added to queue: %s", queue_items) 79 | 80 | return Result(tab) 81 | -------------------------------------------------------------------------------- /mathesis/system/classical/__init__.py: -------------------------------------------------------------------------------- 1 | from mathesis.system.classical import truth_table 2 | 3 | __all__ = ["truth_table"] 4 | -------------------------------------------------------------------------------- /mathesis/system/classical/truth_table.py: -------------------------------------------------------------------------------- 1 | from mathesis.semantics.truth_table.classical import ( 2 | NegationClause, 3 | ConjunctionClause, 4 | DisjunctionClause, 5 | ConditionalClause, 6 | ClassicalTruthTable, 7 | ) 8 | -------------------------------------------------------------------------------- /mathesis/system/intuitionistic/__init__.py: -------------------------------------------------------------------------------- 1 | from mathesis.system.intuitionistic import sequent_calculus 2 | 3 | __all__ = ["sequent_calculus"] 4 | -------------------------------------------------------------------------------- /mathesis/system/intuitionistic/sequent_calculus/__init__.py: -------------------------------------------------------------------------------- 1 | from mathesis.deduction.sequent_calculus import SequentTree 2 | -------------------------------------------------------------------------------- /mathesis/system/intuitionistic/sequent_calculus/rules.py: -------------------------------------------------------------------------------- 1 | from itertools import count 2 | 3 | from mathesis.deduction.sequent_calculus import rules as classical_rules 4 | 5 | 6 | class Weakening: 7 | class Left(classical_rules.Weakening.Left): 8 | def apply(self, target, tip, counter=count(1)): 9 | right = target.sequent_node.sequent.right 10 | assert len(right) == 1, "Right side length of the sequent must be 1" 11 | return super().apply(target, tip, counter=counter) 12 | 13 | class Right(classical_rules.Weakening.Right): 14 | def apply(self, target, tip, counter=count(1)): 15 | right = target.sequent_node.sequent.right 16 | assert len(right) <= 1, "Right side length of the sequent must be 0 or 1" 17 | return super().apply(target, tip, counter=counter) 18 | 19 | 20 | class Negation: 21 | class Left(classical_rules.Negation.Left): 22 | def apply(self, target, tip, counter=count(1)): 23 | right = target.sequent_node.sequent.right 24 | assert len(right) == 0, "Right side length of the sequent must be empty" 25 | return super().apply(target, tip, counter=counter) 26 | 27 | class Right(classical_rules.Negation.Right): 28 | def apply(self, target, tip, counter=count(1)): 29 | right = target.sequent_node.sequent.right 30 | assert len(right) == 1, "Right side length of the sequent must be 0 or 1" 31 | return super().apply(target, tip, counter=counter) 32 | 33 | 34 | class Conjunction: 35 | class Left(classical_rules.Conjunction.Left): 36 | def apply(self, target, tip, counter=count(1)): 37 | right = target.sequent_node.sequent.right 38 | assert len(right) <= 1, "Right side length of the sequent must be 0 or 1" 39 | return super().apply(target, tip, counter=counter) 40 | 41 | class Right(classical_rules.Conjunction.Right): 42 | def apply(self, target, tip, counter=count(1)): 43 | right = target.sequent_node.sequent.right 44 | assert len(right) <= 1, "Right side length of the sequent must be 0 or 1" 45 | return super().apply(target, tip, counter=counter) 46 | 47 | 48 | class Disjunction: 49 | class Left(classical_rules.Disjunction.Left): 50 | def apply(self, target, tip, counter=count(1)): 51 | right = target.sequent_node.sequent.right 52 | assert len(right) <= 1, "Right side length of the sequent must be 0 or 1" 53 | return super().apply(target, tip, counter=counter) 54 | 55 | class Right(classical_rules.Disjunction.Right): 56 | def apply(self, target, tip, counter=count(1)): 57 | right = target.sequent_node.sequent.right 58 | assert len(right) <= 1, "Right side length of the sequent must be 0 or 1" 59 | return super().apply(target, tip, counter=counter) 60 | 61 | 62 | class Conditional: 63 | class Left(classical_rules.Conditional.Left): 64 | def apply(self, target, tip, counter=count(1)): 65 | right = target.sequent_node.sequent.right 66 | assert len(right) <= 1, "Right side length of the sequent must be 0 or 1" 67 | return super().apply(target, tip, counter=counter) 68 | 69 | class Right(classical_rules.Conditional.Right): 70 | def apply(self, target, tip, counter=count(1)): 71 | right = target.sequent_node.sequent.right 72 | assert len(right) <= 1, "Right side length of the sequent must be 0 or 1" 73 | return super().apply(target, tip, counter=counter) 74 | -------------------------------------------------------------------------------- /mathesis/truth_values/__init__.py: -------------------------------------------------------------------------------- 1 | from mathesis.truth_values.boolean import BooleanTruthValues 2 | from mathesis.truth_values.numeric import NumericTruthValues 3 | -------------------------------------------------------------------------------- /mathesis/truth_values/boolean.py: -------------------------------------------------------------------------------- 1 | class BooleanTruthValues: 2 | values = [True, False] 3 | designated_values = [True] 4 | 5 | -------------------------------------------------------------------------------- /mathesis/truth_values/numeric.py: -------------------------------------------------------------------------------- 1 | class NumericTruthValues: 2 | # TODO: support for a range for values and a function for designated_values 3 | def __init__(self, values=[0, 1], designated_values=[1]): 4 | self.values = values 5 | self.designated_values = designated_values 6 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: "Mathesis" 2 | site_description: Python library for formal logic (mathematical logic, philosophical logic), formal semantics, and theorem proving 3 | # strict: true 4 | site_url: https://digitalformallogic.github.io/mathesis/ 5 | 6 | theme: 7 | name: "material" 8 | language: en 9 | custom_dir: "docs/overrides" 10 | # font: 11 | # text: Inter 12 | palette: 13 | - media: "(prefers-color-scheme: light)" 14 | scheme: default 15 | primary: blue grey 16 | accent: blue grey 17 | toggle: 18 | icon: material/lightbulb-outline 19 | name: "Switch to dark mode" 20 | - media: "(prefers-color-scheme: dark)" 21 | scheme: slate 22 | primary: blue grey 23 | accent: blue grey 24 | toggle: 25 | icon: material/lightbulb 26 | name: "Switch to light mode" 27 | features: 28 | - navigation.footer 29 | - navigation.expand 30 | - content.code.copy 31 | # - content.tabs.link 32 | # - content.code.annotate 33 | # - announce.dismiss 34 | # - navigation.tabs 35 | icon: 36 | logo: simple/matrix 37 | repo: fontawesome/brands/github 38 | # favicon: "favicon.png" 39 | 40 | repo_name: digitalformallogic/mathesis 41 | repo_url: https://github.com/digitalformallogic/mathesis 42 | edit_uri: edit/master/docs/ 43 | # extra: 44 | # version: 45 | # provider: mike 46 | 47 | # extra_css: 48 | # - "extra/terminal.css" 49 | # - "extra/tweaks.css" 50 | 51 | extra_javascript: 52 | - js/extra.js 53 | - js/mathjax.js 54 | - https://polyfill.io/v3/polyfill.min.js?features=es6 55 | - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js 56 | # - "extra/fluff.js" 57 | 58 | nav: 59 | - Welcome to Mathesis: index.md 60 | - Get Started: 61 | - Installation: install.md 62 | - Use with JupyterLab/Jupyer Notebook: jupyter.py 63 | # - "Propositional logics": propositional.md 64 | # - "Predicate logics": predicate.md 65 | - Usage: 66 | - Formulas and Grammars: usage/grammars.md 67 | - Truth Tables: usage/truth-tables.py 68 | - Set-theoretic Models: usage/models.py 69 | - 🚧 Possible-world (Kripke) Semantics: usage/kripke.md 70 | - Proof in Tableau: usage/tableaux.md 71 | - Proof in Natural Deduction: usage/natural-deduction.md 72 | - Proof in Sequent Calculus: usage/sequent-calculi.md 73 | - Automated Reasoning: usage/automated-reasoning.md 74 | - Contributing: contributing.md 75 | - Alternatives: alternatives.md 76 | # - 🚧 API Reference: 77 | # - "grammars": reference/grammars/index.md 78 | # - "semantics.truth_table": 79 | # - base: reference/semantics/base.md 80 | # - classical: reference/semantics/classical.md 81 | # - "semantics.model": reference/model.md 82 | 83 | markdown_extensions: 84 | - tables 85 | - toc: 86 | permalink: true 87 | title: Page contents 88 | # - admonition 89 | # - md_in_html 90 | - pymdownx.tasklist: 91 | custom_checkbox: false 92 | - pymdownx.arithmatex: 93 | generic: true 94 | # - pymdownx.details 95 | # - pymdownx.superfences 96 | # - pymdownx.highlight 97 | - pymdownx.extra 98 | - pymdownx.emoji: 99 | emoji_index: !!python/name:material.extensions.emoji.twemoji 100 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 101 | # - pymdownx.tabbed: 102 | # alternate_style: true 103 | 104 | 105 | - pymdownx.superfences: 106 | custom_fences: 107 | - name: python 108 | class: python 109 | validator: !!python/name:markdown_exec.validator 110 | format: !!python/name:markdown_exec.formatter 111 | 112 | watch: 113 | - mathesis 114 | 115 | plugins: 116 | # - mike: 117 | # alias_type: symlink 118 | # canonical_version: latest 119 | - search 120 | - markdown-exec 121 | - mkdocs-jupyter: 122 | execute: true 123 | ignore_h1_titles: true 124 | - exclude: 125 | glob: 126 | - plugins/* 127 | - __pycache__/* 128 | - mkdocstrings: 129 | handlers: 130 | python: 131 | options: 132 | # extensions: 133 | # - griffe_typingdoc 134 | show_root_heading: true 135 | # show_if_no_docstring: true 136 | show_source: false 137 | inherited_members: true 138 | members_order: source 139 | separate_signature: true 140 | unwrap_annotated: true 141 | filters: ["!^_"] 142 | merge_init_into_class: true 143 | docstring_section_style: spacy 144 | signature_crossrefs: true 145 | show_symbol_type_heading: true # insiders 146 | show_symbol_type_toc: true # insiders 147 | show_submodules: true 148 | # - mkdocstrings: 149 | # handlers: 150 | # python: 151 | # paths: [.] 152 | # options: 153 | # extensions: 154 | # - griffe_typingdoc 155 | # show_root_heading: true 156 | # show_if_no_docstring: true 157 | # members_order: source 158 | # separate_signature: true 159 | # filters: ["!^_"] 160 | # docstring_options: 161 | # ignore_init_summary: true 162 | # merge_init_into_class: true 163 | # # inherited_members: true 164 | # show_submodules: true 165 | # docstring_section_style: spacy 166 | # signature_crossrefs: true 167 | # show_symbol_type_heading: true 168 | # show_symbol_type_toc: true 169 | # # extensions: 170 | # # - docs/plugins/griffe_doclinks.py 171 | # - mkdocs-simple-hooks: 172 | # hooks: 173 | # on_pre_build: "docs.plugins.main:on_pre_build" 174 | # on_files: "docs.plugins.main:on_files" 175 | # on_page_markdown: "docs.plugins.main:on_page_markdown" 176 | # - redirects: 177 | # redirect_maps: 178 | # - external-markdown: 179 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mathesis" 3 | version = "0.6.0" 4 | description = "Formal logic library in Python for humans" 5 | authors = [ 6 | {name = "Kentaro Ozeki", email = "32771324+ozekik@users.noreply.github.com"} 7 | ] 8 | readme = "README.md" 9 | license = "MIT" 10 | keywords = ['logic', 'semantics', 'proof', 'philosophy'] 11 | 12 | [project.urls] 13 | homepage = "https://digitalformallogic.github.io/mathesis/" 14 | repository = "https://github.com/digitalformallogic/mathesis" 15 | 16 | [tool.poetry.dependencies] 17 | python = "^3.9" 18 | lark = "^1.1.2" 19 | anytree = "^2.8.0" 20 | prettytable = "^3.3.0" 21 | 22 | [tool.poetry.group.dev.dependencies] 23 | black = { version = "^25", allow-prereleases = true } 24 | ipykernel = "^6.13.0" 25 | 26 | [tool.poetry.group.docs.dependencies] 27 | mkdocs = "^1.5.1" 28 | mkdocs-material = "^9.1.21" 29 | mkdocstrings-python = "^1.2.1" 30 | mkdocs-exclude = "^1.0.2" 31 | markdown-exec = { extras = ["ansi"], version = "^1.8.1" } 32 | mkdocs-jupyter = "^0.25" 33 | griffe-typingdoc = "^0.2.4" 34 | 35 | [build-system] 36 | requires = ["poetry-core>=1.0.0"] 37 | build-backend = "poetry.core.masonry.api" 38 | --------------------------------------------------------------------------------