├── .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 | [](https://github.com/digitalformallogic/mathesis/actions/workflows/ci.yml)
4 | [](https://pypi.org/project/mathesis/)
5 | [](https://digitalformallogic.github.io/mathesis/)
6 | [](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 | [](https://pypi.org/project/mathesis/)
8 | [](https://digitalformallogic.github.io/mathesis/)
9 | [](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 |
--------------------------------------------------------------------------------