Repository: aig-upf/tarski (language: python)

Issue:
Ci improvements
This is a work in progress - don't merge. 

- [x] Migrate CI pipelines from Travis to GH actions.
- [x] Update Python versions used in tests: run unit tests on all active Python versions (https://www.python.org/downloads/), and style checks on the latest version. In particular, 
    - [x] Clean up the mess of requirement versions in the `docs` extra, and try to update versions to newer releases.
- [x] Deploy package to pypi automatically on merge to master.
- [x] Add mypy code analysis
Bug parsing real constant
I got an error running the following code:
```
reader = tarski.io.PDDLReader(raise_on_error=True, strict_with_requirements=False)
reader.parse_domain('domain.pddl')
problem = reader.parse_instance('problem.pddl')
```
Input: [pddl files](https://github.com/aig-upf/tarski/files/6792542/pddl_files.zip)

Possible patch:
```
diff --git a/src/tarski/io/_fstrips/common.py b/src/tarski/io/_fstrips/common.py
index dec1730..3e6485b 100644
--- a/src/tarski/io/_fstrips/common.py
+++ b/src/tarski/io/_fstrips/common.py
@@ -67,7 +67,7 @@ def get_requirements_string(problem):
 def create_number_type(lang):
     """ Creates a sort corresponding to the PDDL "number" """
     # For the moment being, we assume that PDDL "number"s are unbound integers
-    parent = lang.get_sort("Integer")
+    parent = lang.get_sort("Real")
     lower = parent.lower_bound
     upper = parent.upper_bound
     lang.interval("number", parent, lower, upper)
```

Atom parsing failes because of whitespace
When parsing .sas files from Fast-Downward parse_atom in utils/helpers.py raises an UndefinedElement error on atoms with arity higher than one, because the whitespace is not ignored.

I will provide a fix on a fork in a few minutes


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 00000000..d2eaf16a
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,42 @@
+name: "CodeQL"
+
+on:
+  push:
+    branches: [main]
+  pull_request:
+    branches: [main]
+  schedule:
+    #        ┌───────────── minute (0 - 59)
+    #        │  ┌───────────── hour (0 - 23)
+    #        │  │ ┌───────────── day of the month (1 - 31)
+    #        │  │ │ ┌───────────── month (1 - 12 or JAN-DEC)
+    #        │  │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
+    #        │  │ │ │ │
+    #        │  │ │ │ │
+    #        │  │ │ │ │
+    #        *  * * * *
+    - cron: '30 1 * * 0'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'python' ]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+        # Learn more about CodeQL language support at https://git.io/codeql-language-support
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v2
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
new file mode 100644
index 00000000..39f8dd1b
--- /dev/null
+++ b/.github/workflows/coverage.yml
@@ -0,0 +1,43 @@
+name: Code coverage
+
+on:
+  pull_request:
+
+env:
+  DOWNWARD_BENCHMARKS: "/tmp/downward-benchmarks"
+  FSBENCHMARKS: "/tmp/fs-benchmarks"
+
+jobs:
+  coverage:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Clone repo
+        uses: actions/checkout@v2
+
+      - name: Set up Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: '3.x'
+
+      - name: Install dependencies
+        # Install all dependencies, then get some benchmarks that we'll use to run the tests.
+        # Curl below is necessary to download the benchmarks
+        run: |
+          sudo apt-get install -y --no-install-recommends gringo curl
+          python -m pip install --upgrade tox pip setuptools wheel
+          gringo --version
+          tox --version
+          ./scripts/get-benchmarks
+
+      - name: Run tests with coverage enabled
+        run: tox -v -e coverage
+
+      - name: Upload coverage to Codecov
+        uses: codecov/codecov-action@v2
+        with:
+#          env_vars: OS,PYTHON
+          fail_ci_if_error: true
+          files: ./coverage.xml
+          flags: unittests
+          verbose: true
\ No newline at end of file
diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml
new file mode 100644
index 00000000..43e0dd58
--- /dev/null
+++ b/.github/workflows/publish-to-pypi.yml
@@ -0,0 +1,46 @@
+name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI
+
+# Relevant pointers:
+# - https://github.com/pypa/gh-action-pypi-publish
+# - https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
+# - https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
+
+on:
+  # We assume that releases are made from master branch, which is protected against direct pushes, and hence all code
+  # there will have successfully run through all unit test, etc. in the PR-configured workflows
+  release:
+    types: [published]
+
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Clone repo
+        uses: actions/checkout@v2
+
+      - name: Set up Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: '3.10'
+
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install build
+
+      - name: Build package
+        run: python -m build
+
+      # We'll first do a test upload, and only if successful do the real upload
+      - name: Publish package to Test PyPI
+        uses: pypa/gh-action-pypi-publish@release/v1.5.0
+        with:
+          user: __token__
+          password: ${{ secrets.TEST_PYPI_API_TOKEN }}
+          repository_url: https://test.pypi.org/legacy/
+
+      - name: Publish package to PyPI
+        uses: pypa/gh-action-pypi-publish@release/v1.5.0
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 531854ef..2563aaf5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,7 @@
 
+# ply-generated obejcts
+parsetab.py
+
 # Object code
 *.slo
 *.lo
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 72533ee9..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,55 +0,0 @@
-dist: bionic
-language: python
-
-env:
-  global:
-    # Benchmark locations
-    - DOWNWARD_BENCHMARKS="/tmp/downward-benchmarks"
-    - FSBENCHMARKS="/tmp/fs-benchmarks"
-
-matrix:
-  include:
-    # Run the pylint linter with Python 3.6
-    - python: 3.7
-      env: TOXENV=pylint
-
-    # Test documentation building only with Python 3.6
-    - python: 3.7
-      env: TOXENV=docs
-
-    # Test the building of the ANTLR grammars only with Python 3.6
-    - python: 3.7
-      env: TOXENV=antlrgrammars
-
-    # Run the standard pytest tests with different python versions
-    - python: 3.6
-      env: TOXENV=pycoverage # We check code coverage only with python 3.6
-
-    - python: 3.7
-      env: TOXENV=py37
-
-    - python: 3.8
-      env: TOXENV=py38
-
-#  pypy doesn't seem to play nice with scipy?
-#    - python: pypy3.5
-#      env: TOXENV=pypy35 GETBENCHMARKS=true
-
-before_install:
-  # Install Gringo for the general unit tests and also for the documentation
-  - if [[ $TOXENV == py* ]] || [[ $TOXENV == docs ]]; then sudo apt-get install -y --no-install-recommends gringo && gringo --version; fi
-
-  # Install curl for running the codecov script
-  - if [[ $TOXENV == pycoverage ]]; then sudo apt-get install -y --no-install-recommends curl; fi
-
-  # Install pandoc for the generation of the documentation
-  - if [[ $TOXENV == docs ]]; then sudo apt-get install -y --no-install-recommends pandoc; fi
-
-install:
-  - python -m pip install --upgrade tox pip setuptools wheel
-  - tox --version
-  - if [[ $TOXENV == py* ]]; then ./scripts/get-benchmarks; fi  # Download benchmarks only if necessary
-  - if [[ $TOXENV == py* ]]; then pip install Cython --install-option="--no-cython-compile"; fi  # Preinstall Cython if necessary
-
-script:
-  - tox -vv
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e0737f60..7ca844d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Deprecated
 ### Fixed
 
-## [0.7.0]
+
+## [0.8.0] 2022-04-07
+### Changed
+ - Added support for `clingo`'s `pypi` distribution, avoiding the need to install it manually.
+   Some internal interfaces were changed, review these changes if your code is coupled to the ASP-based grounding
+   components (#123). `clingo` now is an `extra` dependency of our package. Use `pip install tarski[clingo]`
+   to install tarski with it. The ASP-based grounding component will fall back to a "manually installed" `clingo`
+   in the `PATH` if the Python package is not present.
+### Added
+ - Temporal planning model:
+   - Added concept of post-condition (i.e. `at end` conditions) `Action` class
+   - Added concept of duration to `Action` class
+   - Added concept of grounding constraints to `Action` class
+ - Naive grounding features:
+   - Added method to exhaustively ground terms with subterms that are constants. For instance, the term `foo(x, a)`
+   where `x` is a variable and `a` is a constant can be now grounded taking into account that the second argument of
+   `foo` is already fixed to `a`.
+ - Error reporting and handling:
+   - `UndefinedTerm`: added name of term as an argument. This enables processing exception data, for instance,
+   to initialise such terms to some value like `infty` or force the failure to ground some structural element in
+   a domain, such as an action.
+ - _Experimental_ support for a SAS writer (work in progress).
+ - _Experimental_ ply-based PDDL parser (work in progress):
+   - Current implementation using `ply` package, version 3.11. Eventually will be refactored for `sly`. Plan is 
+   eventually to parse correctly and generate instance structural elements for PDDL 3.1.
+   - Supported features:
+     - Instantaneous actions
+     - Durative actions
+       - Care has been taken so the parser allows the keyword `at` to be used as a predicate/function/type identifier
+     - Derived predicates
+     - Object fluents
+     - Metrics associated with IPC formulations of temporal planning
+   - Partial support:
+     - Conditional effects
+     - Quantified conditions in preconditions, conditional effects and derived predicates
+     - Arithmetic expressions and effects
+ - New formula visitor: `CollectEqualityAtoms`
+   - What it says on the tin: collects equality atoms that appear as subformulas of a given formula
+ - New optional `pip` dependency: `ply`
+ - `FirstOrderLanguage.is_subtype`: it is now checked if there is a path connecting two types in the type hierarchy. If
+     that is the case, this fact is recorded in the dictionary `indirect_ancestor_sorts`. Whenever a change is made 
+     in the type hierarchy (e.g. adding a new sort or changing the parent of a sort), the cache is invalidated. 
+### Removed
+### Fixed
+ - Fixed issue with `parse_atom` method and whitespace in the description of atoms (#121)
+ - Fixed issue with printing of types in untyped domains (#113)
+ - Use real instead of integer numbers by default when parsing with strict_with_requirements=False (#114)
+ - Fixed issue with equality predicates trying to coerce the right hand side to a constant when the left hand side is
+    a term
+ 
+    
+## [0.7.0] - 2021-05-12
 ### Added
   - Added some basic forward search capabilities (#101).
   - Import psutil module conditionally, to offer better support for non-Linux 
diff --git a/HEADER b/HEADER
new file mode 100644
index 00000000..d41e251e
--- /dev/null
+++ b/HEADER
@@ -0,0 +1,8 @@
+# tarski: An AI Planning Modeling Framework
+#
+# Author: <name> <email address>
+# ----------------------------------------------------------------------------------------------------------------------
+# <path to file>
+#
+# <brief description of the contents of the file>
+# ----------------------------------------------------------------------------------------------------------------------
\ No newline at end of file
diff --git a/README.md b/README.md
index a7653e5c..07fcc6ce 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 
 # Tarski - An AI Planning Modeling Framework 
-[![Build Status](https://travis-ci.com/aig-upf/tarski.svg?branch=master)](https://travis-ci.com/aig-upf/tarski)
+[![Unit Tests](https://github.com/aig-upf/tarski/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/aig-upf/tarski/actions/workflows/unit-tests.yml)
 [![Documentation Status](https://readthedocs.org/projects/tarski/badge/?version=latest)](https://tarski.readthedocs.io/en/latest/?badge=latest)
 [![codecov](https://codecov.io/gh/aig-upf/tarski/branch/master/graph/badge.svg)](https://codecov.io/gh/aig-upf/tarski)
 ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/tarski.svg?style=popout)
@@ -25,5 +25,20 @@ reachability analysis, grounding of first-order representations and problem refo
 **Testing**: Most tests can be run by executing `pytest` on the root directory.
 Alternatively, they can be run through `tox`, for which several testing environments [are defined](tox.ini).
 
+## How to Cite
+
+If you find `tarski` useful in your research, you can cite it with the following `bibtex` entry:
+
+```
+@misc{tarski:github:18,
+  author = {Guillem Franc\'{e}s and Miquel Ramirez and Collaborators},
+  title = {Tarski: An {AI} Planning Modeling Framework},
+  year = {2018},
+  publisher = {{GitHub}},
+  journal = {{GitHub} repository},
+  howpublished = {\url{https://github.com/aig-upf/tarski}}
+}
+```
+
 ## License
 Tarski is licensed under the [Apache-2.0 License](LICENSE).
diff --git a/docs/conf.py b/docs/conf.py
index 64359a33..ecd5acc2 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -28,7 +28,7 @@
 # -- Project information -----------------------------------------------------
 
 project = 'Tarski'
-copyright = '2019-2020, Miquel Ramírez and Guillem Francès'
+copyright = '2019-2022, Miquel Ramírez and Guillem Francès'
 author = 'Miquel Ramírez and Guillem Francès'
 
 # The short X.Y version.
diff --git a/docs/development/releasing.md b/docs/development/releasing.md
index 97bc583d..bb7b817e 100644
--- a/docs/development/releasing.md
+++ b/docs/development/releasing.md
@@ -1,39 +1,23 @@
 
-# Release Process
+# Creating Tarski releases
 
-## Uploading a Tarski release to the Python Package Index
+The release process and upload of packages to `pypi` is semi-automated, and involves creating a pull
+request from `devel` to `master`, then merging it and creating a GH release (that will automatically 
+trigger the `pypi` upload).
+The list of detailed steps is as follows:
 
-Docs: <https://packaging.python.org/tutorials/packaging-projects/>
-
-1. Update the `CHANGELOG.md` file.
-1. Update the version number in `src/tarski/version.py`
-1. Commit. Use message like "Preparing for release 0.6.0".
-1. Go to the `master` branch and merge the `devel` branch.
-1. Tag the release.
-    ```bash
-        export TARSKI_RELEASE="v0.2.0"
-        git tag -a ${TARSKI_RELEASE} -m "Tarski release ${TARSKI_RELEASE}"
-    ```
-        
-1. Run the following instructions from the root directory of the project:
-    ```bash
-        python3 -m pip install --upgrade twine setuptools wheel  # (optional)
-    
-        rm -rf dist/
-        python3 setup.py sdist bdist_wheel
-    
-        # Test first (result will go to https://test.pypi.org/project/tarski/):
-        python3 -m twine upload --skip-existing --repository-url https://test.pypi.org/legacy/ dist/*
-    
-        # Then go!
-        python3 -m twine upload --skip-existing dist/*
-    ```
-
-1. Push all code changes plus the tag to the repo:
-    ```bash
-        git push && git push origin ${TARSKI_RELEASE}
-    ```
-
-1. Check the [Github releases page](https://github.com/aig-upf/tarski/releases) to make sure the new release appears
-   there and can be downwloaded.
+1. Make sure you're in the `devel` branch.
+2. Update the version number in `src/tarski/version.py`
+3. Update the `CHANGELOG.md` file manually to reflect the changes since the last release.
+4. Don't forget to commit & push the changes.
+5. Create a PR against the `master` branch.
+6. Once all workflows have executed successfully, merge the PR. 
+7. Create a [new release](https://github.com/aig-upf/tarski/releases/new) in the GH repo,
+   with an appropriate tag such as `v0.6.0`. The new release will trigger a GH workflow that will
+   automatically upload the package to `pypi`.
 
+## Relevant Pointers
+* <https://packaging.python.org/tutorials/packaging-projects/>
+* <https://github.com/pypa/gh-action-pypi-publish>
+* <https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries>
+* <https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/>
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 00000000..e6a81d90
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,11 @@
+# Tarski Examples
+
+Tarski examples are provided as `Python` "local" executable modules, and thus not need to be installed into some
+folder in the user's `PATH`. To run a example the following command should suffice, issued from within the `examples`
+directory:
+
+```
+$ python3 -m pddl_derived_predicates
+```
+
+Note that depending on how `Python` has been installed in your system you may need to replace `python3` by `python`.
\ No newline at end of file
diff --git a/examples/data/pddl/derived_predicates/min_cut/domain.pddl b/examples/data/pddl/derived_predicates/min_cut/domain.pddl
new file mode 100644
index 00000000..a37fd1d5
--- /dev/null
+++ b/examples/data/pddl/derived_predicates/min_cut/domain.pddl
@@ -0,0 +1,46 @@
+;; https://github.com/dosydon/axiom_benchmarks/blob/master/mincut/domain.pddl
+
+(define (domain min-cut)
+    (:requirements :strips :typing :negative-preconditions :derived-predicates)
+
+    (:types node edge block)
+
+    (:predicates
+    ;; static
+    (adjacent ?e - edge ?f - edge)
+    (edge-from ?e - edge ?n - node)
+    (edge-to ?e - edge ?n - node)
+    (source-node ?n - node)
+    ;; derived:
+    (blocked ?e - edge)
+    (reachable-node ?n - node)
+    (reachable-edge ?e - edge)
+    (isolated ?n - node)
+    )
+
+    (:functions
+    ;; primary state variables:
+    (at ?b - block) - edge
+    )
+
+    (:derived (blocked ?e - edge)
+        (exists (?b - block) (= (at ?b) ?e)))
+
+    (:derived (reachable-node ?n - node) (source-node ?n))
+    (:derived (reachable-node ?n - node)
+        (exists (?e - edge) (and (edge-to ?e ?n)
+                     (not (blocked ?e))
+                     (reachable-edge ?e))))
+    (:derived (reachable-edge ?e - edge)
+        (exists (?n - node) (and (edge-from ?e ?n)
+                     (reachable-node ?n))))
+    (:derived (isolated ?n - node) (not (reachable-node ?n)))
+
+    (:action move
+    :parameters (?b - block ?from - edge ?to - edge)
+    :precondition (and (adjacent ?from ?to)
+              (= (at ?b) ?from))
+    :effect (assign (at ?b) ?to)
+    )
+
+)
\ No newline at end of file
diff --git a/examples/data/pddl/derived_predicates/min_cut/p00.pddl b/examples/data/pddl/derived_predicates/min_cut/p00.pddl
new file mode 100644
index 00000000..7005ca20
--- /dev/null
+++ b/examples/data/pddl/derived_predicates/min_cut/p00.pddl
@@ -0,0 +1,294 @@
+;; cut = [(0,1),(0,10),(0,11)]
+(define (problem min-cut-12-3-99379962)
+ (:domain min-cut)
+ (:objects
+  n0 - node
+  n1 - node
+  n2 - node
+  n3 - node
+  n4 - node
+  n5 - node
+  n6 - node
+  n7 - node
+  n8 - node
+  n9 - node
+  n10 - node
+  n11 - node
+  e-0-1 - edge
+  e-0-10 - edge
+  e-0-11 - edge
+  e-1-0 - edge
+  e-1-2 - edge
+  e-1-6 - edge
+  e-1-7 - edge
+  e-2-1 - edge
+  e-2-3 - edge
+  e-3-2 - edge
+  e-3-8 - edge
+  e-4-5 - edge
+  e-4-7 - edge
+  e-4-11 - edge
+  e-5-4 - edge
+  e-5-6 - edge
+  e-5-7 - edge
+  e-5-8 - edge
+  e-6-1 - edge
+  e-6-5 - edge
+  e-6-7 - edge
+  e-6-10 - edge
+  e-7-1 - edge
+  e-7-4 - edge
+  e-7-5 - edge
+  e-7-6 - edge
+  e-7-10 - edge
+  e-8-3 - edge
+  e-8-5 - edge
+  e-8-9 - edge
+  e-9-8 - edge
+  e-9-10 - edge
+  e-10-0 - edge
+  e-10-6 - edge
+  e-10-7 - edge
+  e-10-9 - edge
+  e-10-11 - edge
+  e-11-0 - edge
+  e-11-4 - edge
+  e-11-10 - edge
+  b0 - block
+  b1 - block
+  b2 - block
+  )
+ (:init
+  (edge-from e-0-1 n0)
+  (edge-to e-0-1 n1)
+  (adjacent e-0-1 e-1-0)
+  (adjacent e-0-1 e-1-2)
+  (adjacent e-0-1 e-1-6)
+  (adjacent e-0-1 e-1-7)
+  (edge-from e-0-10 n0)
+  (edge-to e-0-10 n10)
+  (adjacent e-0-10 e-10-0)
+  (adjacent e-0-10 e-10-6)
+  (adjacent e-0-10 e-10-7)
+  (adjacent e-0-10 e-10-9)
+  (adjacent e-0-10 e-10-11)
+  (edge-from e-0-11 n0)
+  (edge-to e-0-11 n11)
+  (adjacent e-0-11 e-11-0)
+  (adjacent e-0-11 e-11-4)
+  (adjacent e-0-11 e-11-10)
+  (edge-from e-1-0 n1)
+  (edge-to e-1-0 n0)
+  (adjacent e-1-0 e-0-1)
+  (adjacent e-1-0 e-0-10)
+  (adjacent e-1-0 e-0-11)
+  (edge-from e-1-2 n1)
+  (edge-to e-1-2 n2)
+  (adjacent e-1-2 e-2-1)
+  (adjacent e-1-2 e-2-3)
+  (edge-from e-1-6 n1)
+  (edge-to e-1-6 n6)
+  (adjacent e-1-6 e-6-1)
+  (adjacent e-1-6 e-6-5)
+  (adjacent e-1-6 e-6-7)
+  (adjacent e-1-6 e-6-10)
+  (edge-from e-1-7 n1)
+  (edge-to e-1-7 n7)
+  (adjacent e-1-7 e-7-1)
+  (adjacent e-1-7 e-7-4)
+  (adjacent e-1-7 e-7-5)
+  (adjacent e-1-7 e-7-6)
+  (adjacent e-1-7 e-7-10)
+  (edge-from e-2-1 n2)
+  (edge-to e-2-1 n1)
+  (adjacent e-2-1 e-1-0)
+  (adjacent e-2-1 e-1-2)
+  (adjacent e-2-1 e-1-6)
+  (adjacent e-2-1 e-1-7)
+  (edge-from e-2-3 n2)
+  (edge-to e-2-3 n3)
+  (adjacent e-2-3 e-3-2)
+  (adjacent e-2-3 e-3-8)
+  (edge-from e-3-2 n3)
+  (edge-to e-3-2 n2)
+  (adjacent e-3-2 e-2-1)
+  (adjacent e-3-2 e-2-3)
+  (edge-from e-3-8 n3)
+  (edge-to e-3-8 n8)
+  (adjacent e-3-8 e-8-3)
+  (adjacent e-3-8 e-8-5)
+  (adjacent e-3-8 e-8-9)
+  (edge-from e-4-5 n4)
+  (edge-to e-4-5 n5)
+  (adjacent e-4-5 e-5-4)
+  (adjacent e-4-5 e-5-6)
+  (adjacent e-4-5 e-5-7)
+  (adjacent e-4-5 e-5-8)
+  (edge-from e-4-7 n4)
+  (edge-to e-4-7 n7)
+  (adjacent e-4-7 e-7-1)
+  (adjacent e-4-7 e-7-4)
+  (adjacent e-4-7 e-7-5)
+  (adjacent e-4-7 e-7-6)
+  (adjacent e-4-7 e-7-10)
+  (edge-from e-4-11 n4)
+  (edge-to e-4-11 n11)
+  (adjacent e-4-11 e-11-0)
+  (adjacent e-4-11 e-11-4)
+  (adjacent e-4-11 e-11-10)
+  (edge-from e-5-4 n5)
+  (edge-to e-5-4 n4)
+  (adjacent e-5-4 e-4-5)
+  (adjacent e-5-4 e-4-7)
+  (adjacent e-5-4 e-4-11)
+  (edge-from e-5-6 n5)
+  (edge-to e-5-6 n6)
+  (adjacent e-5-6 e-6-1)
+  (adjacent e-5-6 e-6-5)
+  (adjacent e-5-6 e-6-7)
+  (adjacent e-5-6 e-6-10)
+  (edge-from e-5-7 n5)
+  (edge-to e-5-7 n7)
+  (adjacent e-5-7 e-7-1)
+  (adjacent e-5-7 e-7-4)
+  (adjacent e-5-7 e-7-5)
+  (adjacent e-5-7 e-7-6)
+  (adjacent e-5-7 e-7-10)
+  (edge-from e-5-8 n5)
+  (edge-to e-5-8 n8)
+  (adjacent e-5-8 e-8-3)
+  (adjacent e-5-8 e-8-5)
+  (adjacent e-5-8 e-8-9)
+  (edge-from e-6-1 n6)
+  (edge-to e-6-1 n1)
+  (adjacent e-6-1 e-1-0)
+  (adjacent e-6-1 e-1-2)
+  (adjacent e-6-1 e-1-6)
+  (adjacent e-6-1 e-1-7)
+  (edge-from e-6-5 n6)
+  (edge-to e-6-5 n5)
+  (adjacent e-6-5 e-5-4)
+  (adjacent e-6-5 e-5-6)
+  (adjacent e-6-5 e-5-7)
+  (adjacent e-6-5 e-5-8)
+  (edge-from e-6-7 n6)
+  (edge-to e-6-7 n7)
+  (adjacent e-6-7 e-7-1)
+  (adjacent e-6-7 e-7-4)
+  (adjacent e-6-7 e-7-5)
+  (adjacent e-6-7 e-7-6)
+  (adjacent e-6-7 e-7-10)
+  (edge-from e-6-10 n6)
+  (edge-to e-6-10 n10)
+  (adjacent e-6-10 e-10-0)
+  (adjacent e-6-10 e-10-6)
+  (adjacent e-6-10 e-10-7)
+  (adjacent e-6-10 e-10-9)
+  (adjacent e-6-10 e-10-11)
+  (edge-from e-7-1 n7)
+  (edge-to e-7-1 n1)
+  (adjacent e-7-1 e-1-0)
+  (adjacent e-7-1 e-1-2)
+  (adjacent e-7-1 e-1-6)
+  (adjacent e-7-1 e-1-7)
+  (edge-from e-7-4 n7)
+  (edge-to e-7-4 n4)
+  (adjacent e-7-4 e-4-5)
+  (adjacent e-7-4 e-4-7)
+  (adjacent e-7-4 e-4-11)
+  (edge-from e-7-5 n7)
+  (edge-to e-7-5 n5)
+  (adjacent e-7-5 e-5-4)
+  (adjacent e-7-5 e-5-6)
+  (adjacent e-7-5 e-5-7)
+  (adjacent e-7-5 e-5-8)
+  (edge-from e-7-6 n7)
+  (edge-to e-7-6 n6)
+  (adjacent e-7-6 e-6-1)
+  (adjacent e-7-6 e-6-5)
+  (adjacent e-7-6 e-6-7)
+  (adjacent e-7-6 e-6-10)
+  (edge-from e-7-10 n7)
+  (edge-to e-7-10 n10)
+  (adjacent e-7-10 e-10-0)
+  (adjacent e-7-10 e-10-6)
+  (adjacent e-7-10 e-10-7)
+  (adjacent e-7-10 e-10-9)
+  (adjacent e-7-10 e-10-11)
+  (edge-from e-8-3 n8)
+  (edge-to e-8-3 n3)
+  (adjacent e-8-3 e-3-2)
+  (adjacent e-8-3 e-3-8)
+  (edge-from e-8-5 n8)
+  (edge-to e-8-5 n5)
+  (adjacent e-8-5 e-5-4)
+  (adjacent e-8-5 e-5-6)
+  (adjacent e-8-5 e-5-7)
+  (adjacent e-8-5 e-5-8)
+  (edge-from e-8-9 n8)
+  (edge-to e-8-9 n9)
+  (adjacent e-8-9 e-9-8)
+  (adjacent e-8-9 e-9-10)
+  (edge-from e-9-8 n9)
+  (edge-to e-9-8 n8)
+  (adjacent e-9-8 e-8-3)
+  (adjacent e-9-8 e-8-5)
+  (adjacent e-9-8 e-8-9)
+  (edge-from e-9-10 n9)
+  (edge-to e-9-10 n10)
+  (adjacent e-9-10 e-10-0)
+  (adjacent e-9-10 e-10-6)
+  (adjacent e-9-10 e-10-7)
+  (adjacent e-9-10 e-10-9)
+  (adjacent e-9-10 e-10-11)
+  (edge-from e-10-0 n10)
+  (edge-to e-10-0 n0)
+  (adjacent e-10-0 e-0-1)
+  (adjacent e-10-0 e-0-10)
+  (adjacent e-10-0 e-0-11)
+  (edge-from e-10-6 n10)
+  (edge-to e-10-6 n6)
+  (adjacent e-10-6 e-6-1)
+  (adjacent e-10-6 e-6-5)
+  (adjacent e-10-6 e-6-7)
+  (adjacent e-10-6 e-6-10)
+  (edge-from e-10-7 n10)
+  (edge-to e-10-7 n7)
+  (adjacent e-10-7 e-7-1)
+  (adjacent e-10-7 e-7-4)
+  (adjacent e-10-7 e-7-5)
+  (adjacent e-10-7 e-7-6)
+  (adjacent e-10-7 e-7-10)
+  (edge-from e-10-9 n10)
+  (edge-to e-10-9 n9)
+  (adjacent e-10-9 e-9-8)
+  (adjacent e-10-9 e-9-10)
+  (edge-from e-10-11 n10)
+  (edge-to e-10-11 n11)
+  (adjacent e-10-11 e-11-0)
+  (adjacent e-10-11 e-11-4)
+  (adjacent e-10-11 e-11-10)
+  (edge-from e-11-0 n11)
+  (edge-to e-11-0 n0)
+  (adjacent e-11-0 e-0-1)
+  (adjacent e-11-0 e-0-10)
+  (adjacent e-11-0 e-0-11)
+  (edge-from e-11-4 n11)
+  (edge-to e-11-4 n4)
+  (adjacent e-11-4 e-4-5)
+  (adjacent e-11-4 e-4-7)
+  (adjacent e-11-4 e-4-11)
+  (edge-from e-11-10 n11)
+  (edge-to e-11-10 n10)
+  (adjacent e-11-10 e-10-0)
+  (adjacent e-11-10 e-10-6)
+  (adjacent e-11-10 e-10-7)
+  (adjacent e-11-10 e-10-9)
+  (adjacent e-11-10 e-10-11)
+  (= (at b0) e-1-2)
+  (= (at b1) e-3-8)
+  (= (at b2) e-5-7)
+  (source-node n0)
+  )
+ (:goal (isolated n6))
+ )
\ No newline at end of file
diff --git a/examples/data/pddl/temporal/elevators/p01-domain.pddl b/examples/data/pddl/temporal/elevators/p01-domain.pddl
new file mode 100644
index 00000000..a461c648
--- /dev/null
+++ b/examples/data/pddl/temporal/elevators/p01-domain.pddl
@@ -0,0 +1,60 @@
+(define (domain elevators-time-numeric)
+  (:requirements :typing :durative-actions :numeric-fluents)
+  (:types 	elevator - object 
+			slow-elevator fast-elevator - elevator
+   			passenger - object
+          	floor - object
+         )
+
+(:predicates 
+	(passenger-at ?person - passenger ?floor - floor)
+	(boarded ?person - passenger ?lift - elevator)
+	(lift-at ?lift - elevator ?floor - floor)
+	(reachable-floor ?lift - elevator ?floor - floor)
+	(above ?floor1 - floor ?floor2 - floor)
+)
+
+(:functions 
+            (travel-slow ?f1 - floor ?f2 - floor) - number
+            (travel-fast ?f1 - floor ?f2 - floor) - number 
+			(passengers ?lift - elevator) - number
+			(capacity ?lift - elevator) - number
+)
+
+(:durative-action move-up-slow
+  :parameters (?lift - slow-elevator ?f1 - floor ?f2 - floor )
+  :duration (= ?duration (travel-slow ?f1 ?f2))
+  :condition (and (at start (lift-at ?lift ?f1)) (at start (above ?f1 ?f2)) (at start (reachable-floor ?lift ?f2)) )
+  :effect (and (at end (lift-at ?lift ?f2)) (at start (not (lift-at ?lift ?f1))) ))
+
+(:durative-action move-down-slow
+  :parameters (?lift - slow-elevator ?f1 - floor ?f2 - floor )
+  :duration (= ?duration (travel-slow ?f2 ?f1))
+  :condition (and (at start (lift-at ?lift ?f1)) (at start (above ?f2 ?f1)) (at start (reachable-floor ?lift ?f2)) )
+  :effect (and (at end (lift-at ?lift ?f2)) (at start (not (lift-at ?lift ?f1))) ))
+
+(:durative-action move-up-fast
+  :parameters (?lift - fast-elevator ?f1 - floor ?f2 - floor )
+  :duration (= ?duration (travel-fast ?f1 ?f2))
+  :condition (and (at start (lift-at ?lift ?f1)) (at start (above ?f1 ?f2)) (at start (reachable-floor ?lift ?f2)) )
+  :effect (and (at end (lift-at ?lift ?f2)) (at start (not (lift-at ?lift ?f1))) ))
+
+(:durative-action move-down-fast
+  :parameters (?lift - fast-elevator ?f1 - floor ?f2 - floor )
+  :duration (= ?duration (travel-fast ?f2 ?f1))
+  :condition (and (at start (lift-at ?lift ?f1)) (at start (above ?f2 ?f1)) (at start (reachable-floor ?lift ?f2)) )
+  :effect (and (at end (lift-at ?lift ?f2)) (at start (not (lift-at ?lift ?f1))) ))
+
+(:durative-action board
+  :parameters (?p - passenger ?lift - elevator ?f - floor)
+  :duration (= ?duration 1)
+  :condition (and  (over all (lift-at ?lift ?f)) (at start (passenger-at ?p ?f))  (at start (< (passengers ?lift) (capacity ?lift))) )
+  :effect (and (at start (not (passenger-at ?p ?f))) (at end (boarded ?p ?lift)) (at start (increase (passengers ?lift) 1)) ))
+
+(:durative-action leave 
+  :parameters (?p - passenger ?lift - elevator ?f - floor)
+  :duration (= ?duration 1)
+  :condition (and  (over all (lift-at ?lift ?f)) (at start (boarded ?p ?lift)) )
+  :effect (and (at end (passenger-at ?p ?f)) (at start (not (boarded ?p ?lift))) (at end (decrease (passengers ?lift) 1)) ))
+  
+)
diff --git a/examples/data/pddl/temporal/elevators/p01.pddl b/examples/data/pddl/temporal/elevators/p01.pddl
new file mode 100644
index 00000000..c92a3858
--- /dev/null
+++ b/examples/data/pddl/temporal/elevators/p01.pddl
@@ -0,0 +1,75 @@
+(define (problem elevators-time-p8_4_1)
+(:domain elevators-time-numeric)
+
+(:objects 
+f0 f1 f2 f3 f4 f5 f6 f7 f8  - floor
+p0 p1 p2 p3  - passenger
+fast0 fast1  - fast-elevator
+slow0-0 slow1-0 - slow-elevator
+)
+
+(:init
+(above f0 f1) (above f0 f2) (above f0 f3) (above f0 f4) (above f0 f5) (above f0 f6) (above f0 f7) (above f0 f8) 
+(above f1 f2) (above f1 f3) (above f1 f4) (above f1 f5) (above f1 f6) (above f1 f7) (above f1 f8) 
+(above f2 f3) (above f2 f4) (above f2 f5) (above f2 f6) (above f2 f7) (above f2 f8) 
+(above f3 f4) (above f3 f5) (above f3 f6) (above f3 f7) (above f3 f8) 
+(above f4 f5) (above f4 f6) (above f4 f7) (above f4 f8) 
+(above f5 f6) (above f5 f7) (above f5 f8) 
+(above f6 f7) (above f6 f8) 
+(above f7 f8) 
+
+(lift-at fast0 f8)
+(= (passengers fast0) 0)
+(= (capacity fast0) 3)
+(reachable-floor fast0 f0)(reachable-floor fast0 f2)(reachable-floor fast0 f4)(reachable-floor fast0 f6)(reachable-floor fast0 f8)
+
+(lift-at fast1 f6)
+(= (passengers fast1) 0)
+(= (capacity fast1) 3)
+(reachable-floor fast1 f0)(reachable-floor fast1 f2)(reachable-floor fast1 f4)(reachable-floor fast1 f6)(reachable-floor fast1 f8)
+
+(lift-at slow0-0 f4)
+(= (passengers slow0-0) 0)
+(= (capacity slow0-0) 2)
+(reachable-floor slow0-0 f0)(reachable-floor slow0-0 f1)(reachable-floor slow0-0 f2)(reachable-floor slow0-0 f3)(reachable-floor slow0-0 f4)
+
+(lift-at slow1-0 f4)
+(= (passengers slow1-0) 0)
+(= (capacity slow1-0) 2)
+(reachable-floor slow1-0 f4)(reachable-floor slow1-0 f5)(reachable-floor slow1-0 f6)(reachable-floor slow1-0 f7)(reachable-floor slow1-0 f8)
+
+(passenger-at p0 f3)
+(passenger-at p1 f1)
+(passenger-at p2 f7)
+(passenger-at p3 f0)
+
+(= (travel-slow f0 f1) 12) (= (travel-slow f0 f2) 20) (= (travel-slow f0 f3) 28) (= (travel-slow f0 f4) 36)
+(= (travel-slow f1 f2) 12) (= (travel-slow f1 f3) 20) (= (travel-slow f1 f4) 28) (= (travel-slow f2 f3) 12)
+(= (travel-slow f2 f4) 20) (= (travel-slow f3 f4) 12)
+
+(= (travel-slow f4 f5) 12) (= (travel-slow f4 f6) 20) (= (travel-slow f4 f7) 28) (= (travel-slow f4 f8) 36)
+(= (travel-slow f5 f6) 12) (= (travel-slow f5 f7) 20) (= (travel-slow f5 f8) 28) (= (travel-slow f6 f7) 12)
+(= (travel-slow f6 f8) 20) (= (travel-slow f7 f8) 12)
+
+
+(= (travel-fast f0 f2) 11) (= (travel-fast f0 f4) 13) (= (travel-fast f0 f6) 15) (= (travel-fast f0 f8) 17) 
+
+(= (travel-fast f2 f4) 11) (= (travel-fast f2 f6) 13) (= (travel-fast f2 f8) 15) 
+
+(= (travel-fast f4 f6) 11) (= (travel-fast f4 f8) 13) 
+
+(= (travel-fast f6 f8) 11) 
+
+)
+
+(:goal
+(and
+(passenger-at p0 f4)
+(passenger-at p1 f5)
+(passenger-at p2 f6)
+(passenger-at p3 f2)
+))
+
+(:metric minimize (total-time))
+
+)
diff --git a/examples/pddl_derived_predicates/__init__.py b/examples/pddl_derived_predicates/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/pddl_derived_predicates/__main__.py b/examples/pddl_derived_predicates/__main__.py
new file mode 100644
index 00000000..df9d1748
--- /dev/null
+++ b/examples/pddl_derived_predicates/__main__.py
@@ -0,0 +1,88 @@
+#
+# tarski: An AI Planning Modeling Framework
+#
+# Author: Miquel Ramirez <miquel.ramirez@pm.me>
+# ----------------------------------------------------------------------------------------------------------------------
+# examples/pddl_derived_predicates/__main__.py
+#
+# Example documenting the use of Tarski to acquire a planning instance from a PDDL description that contains derived
+# predicates. Extensive inline comments are provided to explain what are the steps involved and what design decisions
+# need to be made by Tarski's users.
+# ----------------------------------------------------------------------------------------------------------------------
+
+import os
+from argparse import ArgumentParser, Namespace
+
+from tarski.io.pddl.parser import PDDLparser, UnsupportedFeature
+from tarski.syntax.visitors import CollectEqualityAtoms
+
+def process_command_line():
+    parser = ArgumentParser(description="Example illustrating acquisition of instance data from PDDL")
+    parser.add_argument("--domain", dest='domain_file', type=str, default='data/pddl/derived_predicates/min_cut/domain.pddl')
+    parser.add_argument("--problem", dest='problem_file', type=str, default='data/pddl/derived_predicates/min_cut/p00.pddl')
+    parser.add_argument("--verbose", dest='verbose', action='store_true')
+    opt = parser.parse_args()
+    return opt
+
+
+def main(opt: Namespace):
+
+    # By default the example processes the instance due to Ivankovid and Haslum in the directory
+    # `examples/data/pddl/derived_predicates/min_cut`. Feel free to experiment with other PDDL specifications
+    # specifying the domain and problem files via the command line.
+    print("Domain file:", opt.domain_file)
+    print("Problem file:", opt.problem_file)
+
+    # The very first step is to obtain a `PDDLParser` instance. Turn the argument to `True` if you want to
+    # see a trace of the work done by the parser
+    parser = PDDLparser(debug=False)
+
+    # Setting up the parser is a two-step process, as it is required to invoke the `build()` method to
+    # set internal data structures and other parameters (such a log file to store error messages etc.).
+    parser.build(logfile="pddl_derived_predicates.parse.log")
+
+    # Now we acquire the text of the pddl domain file and parse it
+    with open(opt.domain_file) as instream:
+        parser.parse(instream.read())
+
+    # and the problem file
+    with open(opt.problem_file) as instream:
+        parser.parse(instream.read())
+
+    # We note that domain and problem sections of a PDDL specification can be combined into one
+    # single file/stream, or directly from a memory buffer, as it best suits. Whatever the modality,
+    # the domain file needs to be parsed first always.
+
+    # once parsed correctly, we can then access the structural elements of the instance (e.g. types,
+    # predicates, etc.)
+    instance_data = parser.instance
+
+    # constants are grouped per type
+    total_constants = 0
+    print("Objects (constant symbols) found in the instance:")
+    for typename, constant_list in instance_data.domains.items():
+        print("Type:", typename, "# constants:", len(constant_list))
+        total_constants += len(constant_list)
+    print("Total constants (objects) in instance:", total_constants)
+
+    print("instance data:")
+    print("# types:", len(instance_data.types))
+    print("# constants:", total_constants)
+    print("# predicates:", len(instance_data.predicates))
+    print("# functions:", len(instance_data.functions))
+    print("# instantaneous actions:", len(instance_data.actions))
+    print("# durative actions:", len(instance_data.durative))
+    print("# derived predicates:", len(instance_data.derived))
+
+    # In this example, we use the instance data to "fill in" an instance of the class `Problem` of
+    # the `fstrips` module, which can then be grounded with the provided grounding algorithms (see
+    # modules `tarski.grounding.lp_grounding` and `tarski.grounding.naive_grounding`.
+
+    # TBC
+
+
+
+
+if __name__ == '__main__':
+    opt = process_command_line()
+    main(opt)
\ No newline at end of file
diff --git a/examples/pddl_temporal_actions/__init__.py b/examples/pddl_temporal_actions/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/pddl_temporal_actions/__main__.py b/examples/pddl_temporal_actions/__main__.py
new file mode 100644
index 00000000..a4aefd2f
--- /dev/null
+++ b/examples/pddl_temporal_actions/__main__.py
@@ -0,0 +1,132 @@
+# tarski: An AI Planning Modeling Framework
+#
+# Author: Miquel Ramirez <miquel.ramirez@pm.me>
+# ----------------------------------------------------------------------------------------------------------------------
+# tarski/examples/pddl_temporal_actions/__main__.py
+#
+# Example illustrating parsing and basic processing of PDDL 2.1 features for modeling temporal planning problems
+# ----------------------------------------------------------------------------------------------------------------------
+
+import os
+import tempfile
+from argparse import ArgumentParser, Namespace
+
+from tarski.io.pddl import Features
+from tarski.io.pddl.parser import PDDLparser, UnsupportedFeature
+from tarski.syntax.visitors import CollectEqualityAtoms
+
+def process_command_line():
+    parser = ArgumentParser(description="Example illustrating acquisition of instance data from PDDL")
+    parser.add_argument("--domain", dest='domain_file', type=str, default='data/pddl/temporal/elevators/p01-domain.pddl')
+    parser.add_argument("--problem", dest='problem_file', type=str, default='data/pddl/temporal/elevators/p01.pddl')
+    parser.add_argument("--verbose", dest='verbose', action='store_true')
+    opt = parser.parse_args()
+    return opt
+
+
+def main(opt: Namespace):
+    parser = PDDLparser(debug=True)
+
+    with tempfile.NamedTemporaryFile() as f:
+        parser.build(logfile=f.name)
+
+        pddl_data = ""
+        with open(opt.domain_file) as instream:
+            pddl_data += instream.read()
+        with open(opt.problem_file) as instream:
+            pddl_data += instream.read()
+
+        parser.parse(pddl_data)
+
+        assert parser.domain_name == 'elevators-time-numeric'
+        assert parser.problem_name == 'elevators-time-p8_4_1'
+        assert Features.DURATIVE_ACTIONS in parser.required_features
+        assert Features.TYPING in parser.required_features
+
+        instance = parser.instance
+        assert instance is not None
+
+        # Now we inspect the PDDL types defined in the domain, and for each of these
+        # what objects were defined in the domain as "constants", or in the problem file as "objects"
+        print("PDDL Types", len(instance.types))
+
+        for t in instance.types:
+            pddl_type = instance.L.get(t)
+            if pddl_type.builtin:
+                print("\t{} [Built-in Type]".format(pddl_type.name))
+                continue
+            print("\t{} Domain:".format(pddl_type.name))
+            print("\t\t{}".format(', '.join([str(obj) for obj in pddl_type.domain()])))
+
+        # The attribute `Instance.functions` is a List[str] object with the names of every
+        # function defined in the PDDL domain. `total-time` does not appear here because it is
+        # not an actual function (as in being a symbol we can obtain an interpretation from a state).
+        print("Functions", len(instance.functions))
+
+        for f in instance.functions:
+            func = instance.L.get(f)
+            print('\t{}: ({}) -> {}'.format(func, ', '.join([str(s) for s in func.domain]), func.codomain))
+
+        # Similarly, `Instance.predicates` is a List[str] object with the names of every predicate
+        # defined in the PDDL domain. Predicates are modelled as functions mapping to the Booleans {0, 1}.
+        print("Predicates", len(instance.predicates))
+
+        for p in instance.predicates:
+            pred = instance.L.get(p)
+            print('\t{} \in ({})'.format(pred, ' x '.join([str(s) for s in pred.domain])))
+
+        # The constants (terms) defined in the domain and problem definitions can be also accessed directly
+        # with the interface below
+        print("Domains", len(instance.domains))
+
+        # Instance.domains is a Dictionary[Sort, List[Constant]] object
+        for t, D in instance.domains.items():
+            obj = instance.L.get(t)
+            dom = [str(obj) for obj in D]
+            print('\t{}: {}'.format(str(obj), ', '.join(dom)))
+
+        # The durative actions in the domain are stored in the `Instance.durative` attribute
+        print("Actions: durative: {}".format(len(instance.durative)))
+
+        # For each action
+        for act in instance.durative:
+            # we have its name
+            print("\tname: {}".format(act.name))
+            # its parameters as a List[Variable] object
+            print("\tparameters: {}".format(act.parameters))
+            # and its duration
+            print("\tduration: {}".format(act.duration))
+            # the preconditions and effects of the `start` event
+            print("\tstart:")
+            # preconditions are given as lists of equality literals
+            print("\t\tprecondition:", ', '.join([str(p.expr) for p in act.at_start.pre]))
+            print("\t\teffects:")
+            for eff in act.at_start.post:
+                print("\t\t\t{} := {}".format(eff.lhs, eff.rhs))
+
+            print("\t\toverall:", ', '.join([str(o.expr) for o in act.overall]))
+
+            print("\tend:")
+            # preconditions are given as lists of equality literals
+            print("\t\tprecondition:", ', '.join([str(p.expr) for p in act.at_end.pre]))
+            print("\t\teffects:")
+            for eff in act.at_end.post:
+                print("\t\t\t{} := {}".format(eff.lhs, eff.rhs))
+
+        print("Initial State literals", len(instance.init))
+        # The initial state is given as a set of tuple of CompoundTerm and a value that can be Boolean or
+        # `number`
+        for l in instance.init:
+            print('\t{}'.format(l))
+
+        eq_atoms_visitor = CollectEqualityAtoms()
+        eq_atoms_visitor.visit(instance.goal)
+        goal_atoms = eq_atoms_visitor.atoms
+        print("Goal literals", len(goal_atoms))
+
+        for l in goal_atoms:
+            print('\t{}'.format(l))
+
+
+if __name__ == '__main__':
+    main(process_command_line())
\ No newline at end of file
diff --git a/examples/sas_without_pddl/__init__.py b/examples/sas_without_pddl/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/sas_without_pddl/__main__.py b/examples/sas_without_pddl/__main__.py
new file mode 100644
index 00000000..c275035b
--- /dev/null
+++ b/examples/sas_without_pddl/__main__.py
@@ -0,0 +1,310 @@
+# ----------------------------------------------------------------------------------------------------------------------
+# examples/sas_without_pddl/__main__.py
+#
+# In this example we construct a sas instance programmatically and write it in a JSON document which can then be
+# transformed or loaded by a backend planner.
+#
+# Extensive inline comments explain the code as we go.
+# ----------------------------------------------------------------------------------------------------------------------
+
+import json
+from argparse import ArgumentParser, Namespace
+from collections import namedtuple
+from itertools import product
+from tarski.evaluators.simple import evaluate
+
+import tarski
+import tarski.model
+
+from tarski.syntax import symref
+from tarski.theories import Theory
+from tarski.syntax.transform.substitutions import substitute_expression, create_substitution
+
+from tarski.sas import Action, Schema
+
+from tarski.util import SymbolIndex
+
+
+def process_command_line():
+    parser = ArgumentParser(description="Example illustrating acquisition of instance data from PDDL")
+    parser.add_argument("--instance", dest='instance', default='4-0')
+    parser.add_argument("--verbose", dest='verbose', action='store_true')
+    opt = parser.parse_args()
+    return opt
+
+
+def check_constraints(C, s, subst):
+    """
+    Returns true if s \models C[x/subst(x)], that is, the result of replacing every variable symbol x by
+    subst(x) in C is satisfiable under s.
+    :param C: A constraint (predicate)
+    :param s: A semantic structure (model) object
+    :param subst: A mapping of variable symbols in C to constants
+    :return:
+    """
+    return all([s[substitute_expression(c, subst)] for c in C])
+
+
+def ground_action_schemas(lang, schemas):
+    """
+    Straightforward grounding by enumeration
+    :param lang: domain theory
+    :param schemas: action schemas to be enumerated
+    :return:
+    """
+
+    actions = []
+    s = tarski.model.create(lang)
+    s.evaluator = evaluate
+
+    for sch in schemas:
+        sch_x = [entry[0] for entry in sch.variables]
+        sch_D = [entry[1] for entry in sch.variables]
+
+        for a in product(*sch_D):
+            subst = create_substitution(sch_x, a)
+            if not check_constraints(sch.constraints, s, subst):
+                continue
+
+            action_a = Action(name=sch.name,
+                              arguments=a,
+                              transitions=[(substitute_expression(x, subst),
+                                            substitute_expression(pre, subst),
+                                            substitute_expression(post, subst)) for x, pre, post in sch.transitions])
+            actions += [action_a]
+
+    return actions
+
+
+
+def dump(lang, actions, initial, goal, objects, fp):
+    """
+    Serializes instance elements (domain theory, actions, initial state and goal condition) into a JSON
+    formatted stream.
+
+    :param lang: Domain theory
+    :param actions: List of ground actions
+    :param init: Initial state, a list of equality literals, given as pairs of terms
+    (symbol of the form $x(\bar{a})$) and values v (constant symbols)
+    :param goal: Goal state, a list of equality literals, given as pairs of term (as above) and values v (as above)
+    :param fp:
+    :return:
+    """
+
+    # `SymbolIndex` is a container class that allows to define a set of objects, where each element is indexed
+    X = SymbolIndex()
+
+    # We identify the set of terms by inspecting 1) action transition constraints, 2) the definition of the
+    # initial state, and 3) the structure of the (conjunctive) goal3 formula.
+    for act in actions:
+        for x, _, _ in act.transitions:
+            if symref(x) in X:
+                continue
+            X.add(symref(x))
+
+    for x, _ in initial:
+        if symref(x) in X:
+            continue
+        X.add(symref(x))
+
+    for x, _ in goal:
+        if symref(x) in X:
+            continue
+        X.add(symref(x))
+
+    # We aggregate all "named" constants in a set (domain) D
+    D = SymbolIndex()
+    for obj in objects:
+        D.add(symref(obj))
+
+    # Depending on the planner, it may be convenient to have definitions of "types" subsets of D, the naturals,
+    # integers or the reals even
+    types_data = [
+        {"name": "object",
+         "domain": [str(o) for o in objects]}
+    ]
+
+    # Here we setup an index that defines the variables of the SAS instance. For each variable we give its
+    # symbolic representation (a string of characters like "on(A)"), and a reference to its domain (set of
+    # objects). Note that a domain does not need to be "tight" and may include many values that never appear
+    # in the set of solutions of transition constraints.
+    vars_data = [
+        {"name": str(x.expr),
+         "type": "object"} for x in X.objects
+    ]
+
+    # Now we provide the list of symbolic representations for each of the actions in the instance
+    action_names = [
+        "{}({})".format(a.name, ",".join([str(arg) for arg in a.arguments])) for a in actions
+    ]
+
+
+    # The transition function is factored as table constraints $Tr(a,x)$, and for each constraint
+    # - indexed by action and variable - we give the pairs of values v and w s.t. for any tuple
+    # (s,a,s') \in Tr, s \models v, s' \models w.
+    trans_data = []
+
+    for k, a in enumerate(actions):
+        for x, v, w in a.transitions:
+            trans_data += [
+                {"a": k,
+                 "x": X.get_index(symref(x)),
+                 "v": D.get_index(symref(v)),
+                 "w": D.get_index(symref(w))}
+            ]
+
+    # We provide the definitions of initial and goal states as lists of pairs of indices into the
+    # set of variables and values in the domain D.
+    init_data = [[X.get_index(symref(x)), D.get_index(symref(v))] for x, v in initial]
+
+    goal_data = [[X.get_index(symref(x)), D.get_index(symref(v))] for x, v in goal]
+
+    # Now we put all the instance data together and build the JSON document
+    doc = {
+        "metadata": {
+            "domain": "blocksworld",
+            "instance": "probBLOCKS-4-0"
+        },
+        "types": types_data,
+        "vars": vars_data,
+        "actions": action_names,
+        "trans": trans_data,
+        "init": init_data,
+        "goal": goal_data
+    }
+
+    json.dump(doc, fp, indent=4)
+
+
+def main(opt: Namespace):
+
+    # We start by creating an instance of the `FirstOrderLanguage` class, that represents
+    # the domain theory for blocksworld.
+    lang = tarski.language("blocksworld", theories=[Theory.EQUALITY])
+
+    # Built-in sorts (`Object`, `Integer`, `Real`) can be accessed via the corresponding
+    # attribute of the domain theory instance
+    object_type = lang.Object
+
+    # We define the `domain` objects, objects that are relevant to every instance of blocksworld
+    # planning problems. Note that we model as objects the table/surface, where blocks can stand
+    # on, and 'nothing' becomes an object too.
+    table = lang.constant('table', object_type)
+    nothing = lang.constant('nothing', object_type)
+
+    # We define `instance` objects (blocks), that is the objects which are associated with a strict
+    # subset of the set of possible blocksworld instances.
+    blocks = [lang.constant(obj, object_type) for obj in ['A', 'B', 'C', 'D']]
+
+    # We factorise the state space of blocksworld with three functions that relate objects
+    # on(x) -> y, what object y is on top of x
+    on = lang.function('on', object_type, object_type)
+    # below(x) -> y, what object y is below x
+    below = lang.function('below', object_type, object_type)
+    # holding() -> x, what object x is being held by the manipulator
+    holding = lang.function('holding', object_type)
+
+    # We define SAS action schemas in a very straightforward way. We define a first order variable
+    # symbol, `target`, to represent the target object of the action `pickup`
+    target = lang.variable('x', object_type)
+
+    # The schema for `pickup` is parametrized by target, which can take as a value any element of the
+    # set `blocks`. The transitions associated with this action refer to three variables, and for
+    # every s, pickup(target), s' it must hold that"
+    # 1) s \models holding() = nothing, s' \models holding() = target
+    # 2) s \models on(target) = nothing, s' \models on(target) = nothing
+    # 3) s \models below(target) = table, s' \models below(target) = nothing
+    pickup = Schema(name='pickup',
+                    variables=[(target, blocks)],
+                    constraints=[],
+                    transitions=[
+                        (holding(), nothing, target),
+                        (on(target), nothing, nothing),
+                        (below(target), table, nothing)
+                    ])
+
+    # The schema for `putdown` is also parametrized by target, which can take any value from the
+    # set `blocks`. Transitions associated with `putdown` refer to three variables, so that for
+    # every s, putdown(target), s' it must hold that:
+    # 1) s \models holding() = target, s' \models holding() = nothing
+    # 2) s \models on(target) = nothing, s' \models on(target) = nothing
+    # 3) s \models below(target) = nothing, s' \models below(target) = table
+    putdown = Schema(name='putdown',
+                     variables=[(target, blocks)],
+                     constraints=[],
+                     transitions=[
+                         (holding(), target, nothing),
+                         (on(target), nothing, nothing),
+                         (below(target), nothing, table)
+                     ])
+
+    # The schema for `stack` is parametrized by `target` and `target2` FO-variables, both taking
+    # values from the set 'blocks'. The schema constraints require that `target` and `target2` are
+    # different. The transitions associated with `stack` are such that:
+    # 1) s \models holding() = target, s' \models holding() = nothing
+    # 2) s \models on(target2) = nothing, s' \models on(target2) = target
+    # 3) s \models below(target) = nothing, s' \models below(target) = target2
+    target2 = lang.variable('y', object_type)
+    stack = Schema(name='stack',
+                   variables=[(target, blocks), (target2, blocks)],
+                   constraints=[target != target2],
+                   transitions=[
+                       (holding(), target, nothing),
+                       (on(target2), nothing, target),
+                       (below(target), nothing, target2)
+                   ])
+
+    # The schema for `unstack` is parametrized by `target` and `target2` FO-variables too, that can take
+    # values from the set 'blocks'. As with `stack`, the schema constraints require that `target` and `target2` are
+    # different. The transitions associated with `unstack` are such that:
+    # 1) s \models holding() = nothing, s' \models holding() = target
+    # 2) s \models on(target2) = target, s' \models on(target2) = nothing
+    # 3) s \models below(target) = target2, s' \models below(target) = nothing
+    unstack = Schema(name='unstack',
+                     variables=[(target, blocks), (target2, blocks)],
+                     constraints=[target != target2],
+                     transitions=[
+                         (holding(), nothing, target),
+                         (on(target2), target, nothing),
+                         (below(target), target2, nothing)
+                     ])
+
+    # Once the schemas are defined, we ground them
+    actions = ground_action_schemas(lang, [pickup, putdown, stack, unstack])
+
+    # We unpack the elements of blocks for convenience
+    A, B, C, D = blocks
+
+    # We define the initial state
+    initial = [
+        (on(C), nothing),
+        (on(A), nothing),
+        (on(B), nothing),
+        (on(D), nothing),
+        (below(C), table),
+        (below(A), table),
+        (below(B), table),
+        (below(D), table),
+        (holding(), nothing)
+    ]
+
+    # and the goal
+    goal = [
+        (on(C), D),
+        (on(B), C),
+        (on(A), B)
+    ]
+
+    print("Ground actions generated:", len(actions))
+
+    # we group together all the instance objects
+    objects = blocks + [table] + [nothing]
+
+    # and we store the result in a JSON document
+    with open("probBLOCKS-4-0.json", "w") as output:
+        dump(lang, actions, initial, goal, objects, output)
+
+
+if __name__ == '__main__':
+    opt = process_command_line()
+    main(opt)
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 3126c348..ea7ab07d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,4 +7,15 @@ requires = ["setuptools", "wheel", "Cython"]  # PEP 508 specifications.
 [tool.black]
 line-length = 120
 # target-version = ['py27', 'py35', 'py36', 'py37', 'py38']
-include = '\.pyi?$'
\ No newline at end of file
+include = '\.pyi?$'
+
+[tool.pytest.ini_options]
+# Logs will be shown for failed test:
+log_format = "%(asctime)s %(levelname)s %(message)s"
+log_date_format = "%Y-%m-%d %H:%M:%S"
+addopts = "-rsxX -q"
+
+markers = [
+    "pddl",
+    "sas",
+]
\ No newline at end of file
diff --git a/scripts/build-docs b/scripts/build-docs
index 54a7a5d6..df43058e 100755
--- a/scripts/build-docs
+++ b/scripts/build-docs
@@ -2,9 +2,6 @@
 
 BUILD_DIR=`pwd`/docs/_build
 
-# Apparently the (dated) version of pandoc in the xenial repos needs the HOME variable to be set explicitly here
-# This could be lifted once we bump up the Ubuntu image used in the travis configuration
 sphinx-build --version
-
-HOME=`pwd` sphinx-build docs ${BUILD_DIR} --color -bhtml
+sphinx-build docs ${BUILD_DIR} --color -bhtml
 echo "Docs available on file://${BUILD_DIR}/index.html"
diff --git a/scripts/report-coverage b/scripts/report-coverage
deleted file mode 100755
index 6be6cd45..00000000
--- a/scripts/report-coverage
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-set -x
-
-# python -m coverage combine
-# python -m coverage xml
-# python -m coverage report -m
-bash <(curl -s https://codecov.io/bash) -Z -X gcov -X coveragepy -X search -X xcode -X gcovout -X fix -f coverage.xml
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 288d5244..7844a643 100644
--- a/setup.py
+++ b/setup.py
@@ -38,12 +38,13 @@ def main():
 
             'Topic :: Scientific/Engineering :: Artificial Intelligence',
 
-            'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
+            "License :: OSI Approved :: Apache Software License",
 
             'Programming Language :: Python :: 3',
-            'Programming Language :: Python :: 3.6',
             'Programming Language :: Python :: 3.7',
             'Programming Language :: Python :: 3.8',
+            'Programming Language :: Python :: 3.9',
+            'Programming Language :: Python :: 3.10',
         ],
 
         packages=find_packages('src'),  # include all packages under src
@@ -60,13 +61,44 @@ def main():
             # Antlr pinned to a specific version to avoid messages "ANTLR runtime and generated code versions disagree"
             # messages. If we want to bump this up, we'll need to regenerate the grammar files with the new version.
             'antlr4-python3-runtime==4.7.2',
+
+
         ],
 
         extras_require={
-            'test': ['pytest', 'tox', 'pytest-cov', 'mypy'],
-            'docs': ['sphinx>=2.1.2', 'recommonmark', 'nbsphinx', 'sphinx_rtd_theme', 'ipykernel', 'ipython'],
-            'arithmetic': ['scipy', 'numpy'],
-            'rddl': ['pyrddl'],
+            'test': [
+                'pytest>=7.0.1',
+                'tox',
+                'pytest-cov',
+                'mypy'
+            ],
+            'docs': [
+                'jinja2==3.0.3',
+                'docutils<0.18',
+                'sphinx==4.4.0',
+                'nbsphinx==0.8.8',
+                'recommonmark==0.7.1',
+                'sphinx_rtd_theme==1.0.0',
+                'ipykernel==6.9.1',
+                'ipython==8.1.0'
+            ],
+            'arithmetic': [
+                'scipy',
+                'numpy'
+            ],
+            'rddl': [
+                "pyrddl @ https://github.com/thiagopbueno/pyrddl/archive/9ccab6a.zip#sha1=a584f90381bf7d48b85976807b9bc6c0cb2761ba"
+            ],
+            'parsegen': [
+                'ply==3.11'
+            ],
+            'clingo': [
+                # Clingo (& gringo) bindings to the clingo solver
+                'clingo>=5.5.1'
+            ],
+            'stubs': [
+                'types-psutil',
+            ]
         },
 
         # This will include non-code files specified in the manifest, see e.g.
diff --git a/src/tarski/__init__.py b/src/tarski/__init__.py
index 8c35d3c0..f5947707 100644
--- a/src/tarski/__init__.py
+++ b/src/tarski/__init__.py
@@ -9,3 +9,6 @@
 from . import fstrips
 
 logging.getLogger(__name__).addHandler(logging.NullHandler())
+
+__all__ = ['__version__', '__version_info__', 'FirstOrderLanguage', 'language', 'Theories',
+           'Function', 'Predicate', 'Formula', 'Term', 'Constant', 'Variable', 'LanguageError', 'fstrips']
diff --git a/src/tarski/analysis/csp.py b/src/tarski/analysis/csp.py
index 3729fe9a..7b9af04a 100644
--- a/src/tarski/analysis/csp.py
+++ b/src/tarski/analysis/csp.py
@@ -64,7 +64,7 @@ def remove_edge(edge):
         # Note that this includes the case where f \subseteq f'.
         # Abiteboul et al. assume that hypergraph is "reduced" and f has thus been compiled into f' at preprocessing;
         # we simply compile it away here on the fly by assuming it is indeed an ear and can be removed.
-        if all(node_counts[node] == 1 for node in f-fprime):
+        if all(node_counts[node] == 1 for node in f - fprime):
             remove_edge(f)
             # print(f'Edge {f} removed in favor of {fprime}')
             return True
diff --git a/src/tarski/analysis/csp_schema.py b/src/tarski/analysis/csp_schema.py
index 9a0f47ae..2dedeeaa 100644
--- a/src/tarski/analysis/csp_schema.py
+++ b/src/tarski/analysis/csp_schema.py
@@ -34,8 +34,8 @@ def get_type_prefix(t):
 
 class CSPInformation:
     def __init__(self):
-        self.name_to_vardata = dict()
-        self.vardata = list()
+        self.name_to_vardata = {}
+        self.vardata = []
         self.constraints = []
         self.effect_relevant_variables = set()
         self.parameter_index = []
@@ -196,7 +196,7 @@ def variable(self, expression, csp, type_, is_reification_var=False):
             lb, ub = self.sort_bounds[expression.sort]
             # Note that Gecode bounds are inclusive, i.e. lb <= x <= ub, whereas in Tarski the upper-bound is exclusive,
             # so we have to fix that here:
-            bounds = (lb, ub-1)
+            bounds = (lb, ub - 1)
         elif isinstance(expression, Constant):
             val = self.object_ids[symref(expression)]
             bounds = (val, val)
@@ -221,7 +221,7 @@ def serialize_schema_csp(self, action, csp, path):
         # sanitized = action.name.lower()
         sanitized = action.name
 
-        with open(os.path.join(path, f'{sanitized}.csp'), 'w') as f:
+        with open(os.path.join(path, f'{sanitized}.csp'), 'w', encoding='utf8') as f:
             print('variables', file=f)
             print(len(csp.vardata), file=f)
             for name, type_, cspvartype, range_ in csp.vardata:
diff --git a/src/tarski/benchmarks/blocksworld.py b/src/tarski/benchmarks/blocksworld.py
index 336f8cea..a7e1ad93 100644
--- a/src/tarski/benchmarks/blocksworld.py
+++ b/src/tarski/benchmarks/blocksworld.py
@@ -24,7 +24,7 @@ def generate_strips_bw_language(nblocks=4):
     lang.predicate('on', object_t, object_t)
     _ = [lang.predicate(p, object_t) for p in "ontable clear holding".split()]
 
-    _ = [lang.constant('b{}'.format(k), object_t) for k in range(1, nblocks+1)]
+    _ = [lang.constant('b{}'.format(k), object_t) for k in range(1, nblocks + 1)]
     return lang
 
 
@@ -41,7 +41,7 @@ def generate_fstrips_bw_language(nblocks=4):
 
     # Table and blocks
     lang.constant('table', place)
-    _ = [lang.constant('b{}'.format(k), block) for k in range(1, nblocks+1)]
+    _ = [lang.constant('b{}'.format(k), block) for k in range(1, nblocks + 1)]
     return lang
 
 
@@ -166,7 +166,7 @@ def generate_random_bw_pattern(lang):
     random.shuffle(blocks)
     pattern = []
     for b in blocks:
-        target, = random.sample(clearplaces, 1)  # A bit of a hack - cannot random.choice from set
+        target = random.choice(list(clearplaces))  # Converting to a list is a bit inefficient, but will work here.
         pattern.append((b, target))
         if target != table:
             clearplaces.remove(target)  # target is no longer clear!
diff --git a/src/tarski/benchmarks/counters.py b/src/tarski/benchmarks/counters.py
index abe2abf8..bdb4ca49 100644
--- a/src/tarski/benchmarks/counters.py
+++ b/src/tarski/benchmarks/counters.py
@@ -20,7 +20,7 @@ def generate_fstrips_counters_language(ncounters=3, upper_bound=None):
     lang.function('value', 'counter', 'val')
     lang.function('max_int', 'val')
 
-    _ = [lang.constant(f'c{k}', 'counter') for k in range(1, ncounters+1)]
+    _ = [lang.constant(f'c{k}', 'counter') for k in range(1, ncounters + 1)]
     return lang
 
 
diff --git a/src/tarski/dl/concepts.py b/src/tarski/dl/concepts.py
index b7f59154..7ad4f1da 100644
--- a/src/tarski/dl/concepts.py
+++ b/src/tarski/dl/concepts.py
@@ -21,8 +21,8 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.name == other.name)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.name == other.name)
 
     def __repr__(self):
         return "{}".format(self.name)
@@ -126,8 +126,8 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.name == other.name)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.name == other.name)
 
     def denotation(self, model):
         return model.compressed(model.primitive_denotation(self), self.ARITY)
@@ -159,8 +159,8 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.name == other.name)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.name == other.name)
 
     def denotation(self, model):
         return model.compressed(model.primitive_denotation(self), self.ARITY)
@@ -192,8 +192,8 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.c == other.c)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.c == other.c)
 
     def denotation(self, model):
         return ~model.compressed_denotation(self.c)
@@ -220,9 +220,9 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.c1 == other.c1 and
-                self.c2 == other.c2)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.c1 == other.c1
+                and self.c2 == other.c2)
 
     def denotation(self, model):
         ext_c1 = model.compressed_denotation(self.c1)
@@ -251,9 +251,9 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.c1 == other.c1 and
-                self.c2 == other.c2)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.c1 == other.c1
+                and self.c2 == other.c2)
 
     def denotation(self, model):
         ext_c1 = model.compressed_denotation(self.c1)
@@ -283,9 +283,9 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.c == other.c and
-                self.r == other.r)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.c == other.c
+                and self.r == other.r)
 
     def denotation(self, model):
         ext_c = model.uncompressed_denotation(self.c)
@@ -317,9 +317,8 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.c == other.c and
-                self.r == other.r)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.c == other.c and self.r == other.r)
 
     def denotation(self, model):
         universe = model.universe()
@@ -354,9 +353,9 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.r1 == other.r1 and
-                self.r2 == other.r2)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.r1 == other.r1
+                and self.r2 == other.r2)
 
     def denotation(self, model):
         universe = model.universe()
@@ -394,8 +393,10 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.name == other.name)
+        return (hasattr(other, 'hash')
+                and self.hash == other.hash
+                and self.__class__ is other.__class__
+                and self.name == other.name)
 
     def denotation(self, model):
         return model.compressed(model.primitive_denotation(self), self.ARITY)
@@ -428,8 +429,8 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.r == other.r)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.r == other.r)
 
     def denotation(self, model):
         ext_r = model.uncompressed_denotation(self.r)
@@ -456,8 +457,8 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.r == other.r)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.r == other.r)
 
     def denotation(self, model):
         ext_r = model.uncompressed_denotation(self.r)
@@ -486,9 +487,9 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.r1 == other.r1 and
-                self.r2 == other.r2)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.r1 == other.r1
+                and self.r2 == other.r2)
 
     def denotation(self, model):
         ext_r1 = model.uncompressed_denotation(self.r1)
@@ -525,9 +526,9 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.c == other.c and
-                self.r == other.r)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.c == other.c
+                and self.r == other.r)
 
     def denotation(self, model):
         ext_c = model.uncompressed_denotation(self.c)
diff --git a/src/tarski/dl/factory.py b/src/tarski/dl/factory.py
index d7a8314b..becbcae9 100644
--- a/src/tarski/dl/factory.py
+++ b/src/tarski/dl/factory.py
@@ -17,10 +17,10 @@ def compute_dl_vocabulary(lang):
     """ Return the DL vocabulary for the given language.
     This is the list of all predicates of arity 0, 1 and 2, all types, and all functions of arity 0 and 1
     """
-    v = [(p.symbol, p) for p in lang.predicates if not builtins.is_builtin_predicate(p) and p.arity in (0, 1, 2)] +\
-        [(f.symbol, f) for f in lang.functions if not builtins.is_builtin_function(f) and f.arity in (0, 1) and
-         f.symbol != "total-cost"] + \
-        [(s.name, s) for s in lang.sorts if not s.builtin and s.name != 'number']
+    v = [(p.symbol, p) for p in lang.predicates if not builtins.is_builtin_predicate(p) and p.arity in (0, 1, 2)] \
+        + [(f.symbol, f) for f in lang.functions if not builtins.is_builtin_function(f) and f.arity in (0, 1)
+           and f.symbol != "total-cost"] \
+        + [(s.name, s) for s in lang.sorts if not s.builtin and s.name != 'number']
     return dict(v)
 
 
diff --git a/src/tarski/dl/features.py b/src/tarski/dl/features.py
index 4a90d9c0..6b620c42 100644
--- a/src/tarski/dl/features.py
+++ b/src/tarski/dl/features.py
@@ -70,15 +70,16 @@ def compute_bool_feature_diff(x, y):
 
 
 def are_feature_changes_analogous(x, y):
-    return x == y or \
-           (x == FeatureValueChange.DEL and y == FeatureValueChange.DEC) or \
-           (x == FeatureValueChange.DEC and y == FeatureValueChange.DEL) or \
-           (x == FeatureValueChange.ADD and y == FeatureValueChange.INC) or \
-           (x == FeatureValueChange.INC and y == FeatureValueChange.ADD)
+    return (x == y
+            or (x == FeatureValueChange.DEL and y == FeatureValueChange.DEC)
+            or (x == FeatureValueChange.DEC and y == FeatureValueChange.DEL)
+            or (x == FeatureValueChange.ADD and y == FeatureValueChange.INC)
+            or (x == FeatureValueChange.INC and y == FeatureValueChange.ADD))
 
 
 class ConceptCardinalityFeature(Feature):
     """ A numeric feature that reflects the cardinality of a set of objects defined by a concept """
+
     def __init__(self, c):
         assert isinstance(c, Concept)
         self.c = c
@@ -99,8 +100,8 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.c == other.c)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.c == other.c)
 
     def concept(self):
         return self.c
@@ -133,8 +134,8 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.c == other.c)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.c == other.c)
 
     def concept(self):
         return self.c
@@ -143,6 +144,7 @@ def complexity(self):
         # The complexity of a binary feature is the complexity of the underlying concept
         return self.c.size
 
+
 # NOT YET FULLY IMPLEMENTED:
 #
 # class IntegerVariableFeature(Feature):
@@ -189,8 +191,8 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.c1 == other.c1 and self.r == other.r and self.c2 == other.c2)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.c1 == other.c1 and self.r == other.r and self.c2 == other.c2)
 
     def denotation(self, model):
         """ The value of the feature is the min distance between any object in the extension of c1 and any object
@@ -231,8 +233,8 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.f1 == other.f1 and self.f2 == other.f2)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.f1 == other.f1 and self.f2 == other.f2)
 
     def denotation(self, model):
         """ The value of the feature is f1 < f2 """
@@ -275,8 +277,8 @@ def __hash__(self):
         return self.hash
 
     def __eq__(self, other):
-        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__ and
-                self.atom == other.atom)
+        return (hasattr(other, 'hash') and self.hash == other.hash and self.__class__ is other.__class__
+                and self.atom == other.atom)
 
     def complexity(self):
         return 1
diff --git a/src/tarski/errors.py b/src/tarski/errors.py
index 3b61b45e..4e5c9746 100644
--- a/src/tarski/errors.py
+++ b/src/tarski/errors.py
@@ -55,6 +55,7 @@ def __init__(self, name, other=None, msg=None):
 class UndefinedElement(SyntacticError):
     def __init__(self, name, msg=None):
         super().__init__(f'Undefined element "{name}" (error msg: {msg})')
+        self.name = name
 
 
 class UnexpectedElementType(SyntacticError):
@@ -156,6 +157,10 @@ class OutOfTimeError(TarskiError):
     pass
 
 
+class ArgumentError(TarskiError):
+    pass
+
+
 # class WrongTermUsageError(TarskiError):
 #     def __init__(self):
 #         super().__init__("""Tarski terms overload the equality operator `__eq__` to allow the
diff --git a/src/tarski/fol.py b/src/tarski/fol.py
index 501580c7..38c3f6cd 100644
--- a/src/tarski/fol.py
+++ b/src/tarski/fol.py
@@ -2,7 +2,7 @@
 import copy
 import itertools
 from collections import defaultdict, OrderedDict
-from typing import Union
+from typing import Union, cast
 
 from . import errors as err
 from .errors import UndefinedElement
@@ -19,17 +19,18 @@ def __init__(self, name=None):
         self._sorts = {}
 
         # A mapping between each sort and its single immediate parent
-        self.immediate_parent = dict()
+        self.immediate_parent = {}
 
         # ancestor_sorts[t] is a set containing all supertypes of sort 't', but NOT 't'
         self.ancestor_sorts = defaultdict(set)
+        self.indirect_ancestor_sorts = defaultdict(set)
 
         self._functions = {}
         self._predicates = {}
         self._constants = OrderedDict()
 
-        self._operators = dict()
-        self._global_index = dict()
+        self._operators = {}
+        self._global_index = {}
         self._element_containers = {Sort: self._sorts,
                                     Function: self._functions,
                                     Predicate: self._predicates}
@@ -66,7 +67,7 @@ def __deepcopy__(self, memo):
 
     def deepcopy(self):
         """ Use this method instead of copy.deepcopy() if you need a true deep-copy of the language """
-        memo = dict()
+        memo = {}
         newone = type(self)()
         memo[id(self)] = newone
         for k, v in self.__dict__.items():
@@ -99,6 +100,7 @@ def _attach_object_sort(self):
         self._global_index['object'] = sort
         self.immediate_parent[sort] = None
         self.ancestor_sorts[sort] = set()
+        self.indirect_ancestor_sorts[sort] = set()
 
     @property
     def Object(self):
@@ -157,13 +159,15 @@ def get_sort(self, name: str) -> Sort:
             raise err.UndefinedSort(name)
         return self._sorts[name]
 
-    def interval(self, name, parent: Interval, lower_bound, upper_bound):
+    def interval(self, name: str, parent: Union[Interval, str], lower_bound, upper_bound) -> Interval:
         """ Create a (bound) interval sort.
 
         We allow only the new sort to derive from the built-in natural, integer or real sorts.
         """
         self._check_name_not_defined(name, self._sorts, err.DuplicateSortDefinition)
-        parent = self._retrieve_sort(parent)
+
+        # Cast just for type-hinting purposes. The cast will be enforced in the type check in the line below.
+        parent = cast(Interval, self._retrieve_sort(parent))
 
         if parent not in (self.Real, self.Natural, self.Integer):
             raise err.SemanticError("Only intervals derived or real, integer or naturals are allowed")
@@ -198,6 +202,9 @@ def set_parent(self, sort: Sort, parent: Sort, overwrite=False):
 
         self.immediate_parent[sort] = parent
         self.ancestor_sorts[sort].update(inclusion_closure(parent))
+        for s in self.indirect_ancestor_sorts:
+            self.indirect_ancestor_sorts[s] = set()
+        self.indirect_ancestor_sorts[sort] = set()
 
     def _retrieve_sort(self, obj: Union[Sort, str]) -> Sort:
         return self._retrieve_object(obj, Sort)
@@ -344,7 +351,27 @@ def most_restricted_type(self, t1, t2):
     def is_subtype(self, t, st):
         t = self._retrieve_sort(t)
         st = self._retrieve_sort(st)
-        return t == st or self.is_strict_subtype(t, st)
+        return t == st or self.is_strict_subtype(t, st) or self.connected_in_type_hierarchy(t, st)
+
+    def connected_in_type_hierarchy(self, t_0, t_goal):
+        """
+        Checks if there is a path in the type hierarchy between t_0 and t_goal
+        :param t_0:
+        :param t_goal:
+        :return:
+        """
+        if t_goal in self.indirect_ancestor_sorts[t_0]:
+            return True
+        OPEN = [t for t in self.ancestor_sorts[t_0]]
+        while len(OPEN) != 0:
+            t = OPEN.pop()
+            if t == t_goal:
+                self.indirect_ancestor_sorts[t_0].add(t_goal)
+                return True
+            for t2 in self.ancestor_sorts[t]:
+                if t2 not in OPEN:
+                    OPEN += [t2]
+        return False
 
     def is_strict_subtype(self, t, st):
         t = self._retrieve_sort(t)
diff --git a/src/tarski/fstrips/manipulation/simplify.py b/src/tarski/fstrips/manipulation/simplify.py
index 5d9fa5fd..64830d9a 100644
--- a/src/tarski/fstrips/manipulation/simplify.py
+++ b/src/tarski/fstrips/manipulation/simplify.py
@@ -1,6 +1,6 @@
 import copy
 
-from multipledispatch import dispatch
+from multipledispatch import dispatch  # type: ignore
 
 from ..fstrips import AddEffect, DelEffect, UniversalEffect, FunctionalEffect
 from ..ops import collect_all_symbols, compute_number_potential_groundings
@@ -87,7 +87,8 @@ def simplify_action(self, action, inplace=False):
             return None
 
         # Filter out those effects that are None, e.g. because they are not applicable:
-        simple.effects = list(filter(None.__ne__, (self.simplify_effect(eff, inplace=True) for eff in simple.effects)))
+        simple.effects = [x for x in (self.simplify_effect(eff, inplace=True) for eff in simple.effects)
+                          if x is not None]
 
         if not simple.effects:  # If not effects remain, the action is useless
             return None
@@ -173,8 +174,8 @@ def simplify_effect(self, effect, inplace=True):
 
         if isinstance(effect, UniversalEffect):
             # Go recursively to the universally quantified effects, filter those that are inapplicable
-            effect.effects = list(filter(None.__ne__,
-                                         (self.simplify_effect(eff, inplace=True) for eff in effect.effects)))
+            effect.effects = [x for x in (self.simplify_effect(eff, inplace=True) for eff in effect.effects)
+                              if x is not None]
             return effect
 
         raise RuntimeError(f'Effect "{effect}" of type "{type(effect)}" cannot be analysed')
@@ -197,11 +198,11 @@ def simplify_existential_quantification(node, inplace=True):
 class ExistentialQuantificationSimplifier(FOLWalker):
     """ Replaces a formula of the form ∃x.φ[x] ∧ x = t by the formula φ[x/t]. """
     @dispatch(object)
-    def visit(self, node):  # pylint: disable-msg=E0102
+    def visit(self, node):  # pylint: disable-msg=E0102  # noqa: F811
         return self.default_handler(node)
 
-    @dispatch(QuantifiedFormula)
-    def visit(self, node: QuantifiedFormula):  # pylint: disable-msg=E0102
+    @dispatch(QuantifiedFormula)  # type: ignore
+    def visit(self, node: QuantifiedFormula):  # pylint: disable-msg=E0102  # noqa: F811
         if node.quantifier == Quantifier.Forall:
             return node
 
diff --git a/src/tarski/fstrips/ops.py b/src/tarski/fstrips/ops.py
index 7d61053f..89588364 100644
--- a/src/tarski/fstrips/ops.py
+++ b/src/tarski/fstrips/ops.py
@@ -5,12 +5,13 @@
 # At the moment we're using the "multipledispatch" package to implement single-argument dispatching.
 # Whenever we move to support Python 3.8+, we could directly use
 #     https://docs.python.org/3/library/functools.html#functools.singledispatchmethod
-from multipledispatch import dispatch
+from multipledispatch import dispatch  # type: ignore
 
 from .walker import ProblemWalker
 from ..syntax import Predicate, Function, CompoundTerm, Atom
 from .problem import Problem
 from . import fstrips as fs
+from .derived import Derived
 
 
 def collect_all_symbols(problem: Problem, include_builtin=False) -> Set[Union[Predicate, Function]]:
@@ -44,17 +45,17 @@ def __init__(self):
         super().__init__()
         self.symbols = set()
 
-    @dispatch(object)
+    @dispatch(object)  # type: ignore
     def visit(self, node):  # pylint: disable-msg=E0102
         return self.default_handler(node)
 
-    @dispatch(CompoundTerm)
-    def visit(self, node):  # pylint: disable-msg=E0102
+    @dispatch(CompoundTerm)  # type: ignore
+    def visit(self, node):  # pylint: disable-msg=E0102  # noqa: F811
         self.symbols.add(node.symbol)
         return node
 
-    @dispatch(Atom)
-    def visit(self, node):  # pylint: disable-msg=E0102
+    @dispatch(Atom)  # type: ignore
+    def visit(self, node):  # pylint: disable-msg=E0102  # noqa: F811
         self.symbols.add(node.symbol)
         return node
 
@@ -65,35 +66,40 @@ def __init__(self):
         super().__init__()
         self.symbols = set()
 
-    @dispatch(object)
-    def visit(self, node):  # pylint: disable-msg=E0102
+    @dispatch(object)  # type: ignore
+    def visit(self, node):  # pylint: disable-msg=E0102  # noqa: F811
         return self.default_handler(node)
 
-    @dispatch(fs.AddEffect)
-    def visit(self, node):  # pylint: disable-msg=E0102
+    @dispatch(fs.AddEffect)  # type: ignore
+    def visit(self, node):  # pylint: disable-msg=E0102  # noqa: F811
         self.symbols.add(node.atom.symbol)
         return node
 
-    @dispatch(fs.DelEffect)
-    def visit(self, node):  # pylint: disable-msg=E0102
+    @dispatch(fs.DelEffect)  # type: ignore
+    def visit(self, node):  # pylint: disable-msg=E0102  # noqa: F811
         self.symbols.add(node.atom.symbol)
         return node
 
-    @dispatch(fs.FunctionalEffect)
-    def visit(self, node):  # pylint: disable-msg=E0102
+    @dispatch(fs.FunctionalEffect)  # type: ignore
+    def visit(self, node):  # pylint: disable-msg=E0102  # noqa: F811
         self.symbols.add(node.lhs.symbol)
         return node
 
-    @dispatch(fs.ChoiceEffect)
-    def visit(self, node):  # pylint: disable-msg=E0102
+    @dispatch(fs.ChoiceEffect)  # type: ignore
+    def visit(self, node):  # pylint: disable-msg=E0102  # noqa: F811
         self.symbols.add(node.obj.symbol)
         return node
 
-    @dispatch(fs.LinearEffect)
-    def visit(self, node):  # pylint: disable-msg=E0102
+    @dispatch(fs.LinearEffect)  # type: ignore
+    def visit(self, node):  # pylint: disable-msg=E0102  # noqa: F811
         self.symbols.update(lhs.symbol for lhs in node.y[:, 0])
         return node
 
+    @dispatch(Derived)  # type: ignore
+    def visit(self, node):  # pylint: disable-msg=E0102  # noqa: F811
+        self.symbols.update(node.predicate)
+        return node
+
 
 def compute_number_potential_groundings(signature):
     cardinalities = [len(list(s.domain())) for s in signature]
diff --git a/src/tarski/fstrips/representation.py b/src/tarski/fstrips/representation.py
index 99003e89..0f9eead5 100644
--- a/src/tarski/fstrips/representation.py
+++ b/src/tarski/fstrips/representation.py
@@ -1,5 +1,5 @@
 import copy
-from typing import Set, Union, Tuple, Optional
+from typing import Set, Union, Tuple, Optional, List
 
 from ..errors import TarskiError
 from .problem import Problem
@@ -8,6 +8,7 @@
     VariableBinding, Constant, Tautology, land, Term
 from ..syntax.ops import collect_unique_nodes, flatten, free_variables, all_variables
 from ..syntax.sorts import compute_signature_bindings
+from ..syntax.symrefs import TermReference
 from ..syntax.transform.substitutions import enumerate_substitutions
 from ..syntax.transform.substitutions import substitute_expression as fol_substitute_expression
 from ..syntax.util import get_symbols
@@ -117,7 +118,7 @@ def is_strips_effect_set(effects):
 
 def compute_effect_set_conflicts(effects):
     """ """
-    polarities = dict()
+    polarities = {}
     conflicts = set()
     for eff in effects:
         if not is_atomic_effect(eff) or not is_propositional_effect(eff):
@@ -142,7 +143,15 @@ def transform_operator_to_strips(action: Action):
 
 def is_literal(phi: Formula):
     """ Return true iff the given formula is a literal """
-    return isinstance(phi, Atom) or (is_neg(phi) and isinstance(phi.subformulas[0], Atom))
+    if isinstance(phi, Atom):
+        return True
+
+    if is_neg(phi):
+        # This assert prevents type checkers from complaining
+        assert isinstance(phi, CompoundFormula)
+        return isinstance(phi.subformulas[0], Atom)
+
+    return False
 
 
 def is_ground(element):
@@ -212,13 +221,13 @@ def collect_literals_from_conjunction(phi: Formula) -> Optional[Set[Tuple[Atom,
     The returned list consists of one tuple (a, p) for each literal in the conjunction phi, where a is the literal atom
     and p its polarity (i.e. True if positive, False if negative)
     """
-    literals = set()  # type: Set[Tuple[Atom, bool]]
+    literals: Set[Tuple[Atom, bool]] = set()
     if _collect_literals_from_conjunction(phi, literals):
         return literals
     return None
 
 
-def _collect_literals_from_conjunction(f, literals: Set[Tuple[Atom, bool]]):
+def _collect_literals_from_conjunction(f, literals: Set[Tuple[Atom, bool]]) -> bool:
     if isinstance(f, Atom):
         literals.add((f, True))
     elif is_neg(f) and isinstance(f.subformulas[0], Atom):
@@ -233,9 +242,11 @@ def _collect_literals_from_conjunction(f, literals: Set[Tuple[Atom, bool]]):
 
 
 def classify_atom_occurrences_in_formula(phi: Formula):
-    """ Classify atoms in the given formula according to whether they occur as positive or negative literals
-    """
-    pos, neg, fun = set(), set(), set()
+    """ Classify atoms in the given formula according to whether they occur as positive or negative literals. """
+    pos: Set[TermReference] = set()
+    neg: Set[TermReference] = set()
+    fun: Set[TermReference] = set()
+
     for atom, truthval in collect_literals_from_conjunction(phi):
         if atom.predicate.builtin:
             fun.update(symref(t) for t in collect_unique_nodes(atom, lambda x: isinstance(x, CompoundTerm)))
@@ -259,11 +270,11 @@ def is_quantifier_free(phi: Formula):
     return len(collect_unique_nodes(phi, lambda x: isinstance(x, QuantifiedFormula))) == 0
 
 
-def collect_effect_free_parameters(action: Action):
-    """ Return the "effect-free" parameters of the given action schema.
-    These are the set of action parameters (logical variables) that do not appear on any effect of the action.
+def collect_effect_free_parameters(action: Action) -> Set[TermReference]:
+    """ Return the set of action parameters (logical variables) in the given action schema that
+     do not appear on any effect of the action.
     """
-    free = set()
+    free: Set[TermReference] = set()
     for eff in action.effects:
         _collect_effect_free_variables(eff, free)
     parameters = {symref(x) for x in action.parameters}
@@ -307,14 +318,14 @@ def project_away_effect_free_variables_from_problem(problem: Problem, inplace=Fa
     return projected
 
 
-def collect_effect_free_variables(eff: fs.BaseEffect):
+def collect_effect_free_variables(eff: fs.BaseEffect) -> Set[TermReference]:
     """ Return the set of all variables that appear free in the given effect. """
     free = set()
     _collect_effect_free_variables(eff, free)
     return free
 
 
-def _collect_effect_free_variables(eff: fs.BaseEffect, free: Set):
+def _collect_effect_free_variables(eff: fs.BaseEffect, free: Set[TermReference]):
     """
     """
     if isinstance(eff, (fs.AddEffect, fs.DelEffect)):
@@ -329,7 +340,7 @@ def _collect_effect_free_variables(eff: fs.BaseEffect, free: Set):
 
     elif isinstance(eff, fs.UniversalEffect):
         bound = {symref(x) for x in eff.variables}
-        free_in_sub = set()
+        free_in_sub: Set[TermReference] = set()
         for sub in eff.effects:
             _collect_effect_free_variables(sub, free_in_sub)
         free.update(free_in_sub - bound)
@@ -344,7 +355,7 @@ def collect_all_function_names(expression, output):
     output.update(f.symbol.name for f in terms)
 
 
-def identify_cost_related_functions(problem: Problem):
+def identify_cost_related_functions(problem: Problem) -> Set[str]:
     """ Return a list of those function symbols that are *only* used in effects that relate to the special
     "total-cost" function. """
     functions = list(get_symbols(problem.language, type_='function', include_builtin=False))
@@ -412,7 +423,7 @@ def compile_negated_preconditions_away(problem: Problem, inplace=False):
     problem = copy.deepcopy(problem) if not inplace else problem
 
     # First compile the action preconditions away
-    negpreds = dict()
+    negpreds = {}
     newactions = []
     for aname, action in problem.actions.items():
         newactions.append((aname, compile_action_negated_preconditions_away(action, negpreds, inplace=True)))
@@ -437,7 +448,7 @@ def compile_negated_preconditions_away(problem: Problem, inplace=False):
 
 def update_action_effects_with_negated_counterparts(action: Action, negpreds):
     """ """
-    neweffects = []
+    neweffects: List[SingleEffect] = []
     for eff in action.effects:
         if not is_propositional_effect(eff):
             raise RepresentationError(f"Don't know how to update negated counterpart atoms for effect {eff}")
diff --git a/src/tarski/fstrips/walker.py b/src/tarski/fstrips/walker.py
index b219d0fb..67f86b07 100644
--- a/src/tarski/fstrips/walker.py
+++ b/src/tarski/fstrips/walker.py
@@ -66,7 +66,8 @@ def _donothing(self, node):
         return node
 
     def run(self, expression, inplace=True):
-        from . import Action, BaseEffect, Problem  # pylint: disable=import-outside-toplevel  # Avoiding circular references
+        # Avoiding circular references:
+        from . import Action, BaseEffect, Problem  # pylint: disable=import-outside-toplevel
         from ..syntax import Formula, Term  # pylint: disable=import-outside-toplevel  # Avoiding circular references
         # Simply dispatch according to type
         expression = expression if inplace else copy.deepcopy(expression)
diff --git a/src/tarski/grounding/lp_grounding.py b/src/tarski/grounding/lp_grounding.py
index 18a54a39..fb602afa 100644
--- a/src/tarski/grounding/lp_grounding.py
+++ b/src/tarski/grounding/lp_grounding.py
@@ -51,7 +51,7 @@ def ground_actions(self):
                                'configured with ground_actions=False')
         model = self._solve_lp()
         # This will take care of the case where there is not ground action from some schema
-        groundings = dict()
+        groundings = {}
         for k in self.problem.actions.keys():
             key = "action_" + k
             groundings[k] = model[key] if key in model else set()
@@ -66,7 +66,7 @@ def _solve_lp(self):
         if self.model is None:
             lp, tr = create_reachability_lp(self.problem, self.do_ground_actions, self.include_variable_inequalities)
             model_filename, theory_filename = run_clingo(lp)
-            self.model = parse_model(model_filename, tr)
+            self.model = parse_model(filename=model_filename, symbol_mapping=tr)
 
             # Remove the input and output files for Gringo
             silentremove(model_filename)
@@ -88,6 +88,7 @@ def compute_action_groundings(problem, include_variable_inequalities=False):
 
 
 def ground_problem_schemas_into_plain_operators(problem, include_variable_inequalities=False):
+    # pylint: disable=import-outside-toplevel
     from ..syntax.transform.action_grounding import ground_schema_into_plain_operator_from_grounding
     action_groundings = compute_action_groundings(problem, include_variable_inequalities)
     operators = []
diff --git a/src/tarski/grounding/naive_grounding.py b/src/tarski/grounding/naive_grounding.py
index 41e8a70b..07e12e45 100644
--- a/src/tarski/grounding/naive_grounding.py
+++ b/src/tarski/grounding/naive_grounding.py
@@ -2,6 +2,7 @@
  Classes and methods related to the naive grounding strategy of planning problems.
 """
 import itertools
+from typing import List
 
 from ..grounding.ops import approximate_symbol_fluency
 from ..syntax import Constant, Variable, CompoundTerm, Atom, create_substitution, termlists_are_equal, termlist_hash
@@ -130,7 +131,7 @@ def ground_state_variables(self):
     def ground_actions(self):
         """  Return a dictionary mapping each action schema of the problem to the set of parameter groundings that
         make that schema a possible ground action. """
-        groundings = dict()
+        groundings = {}
         for aname, action in self.problem.actions.items():
             domains = [p.sort.domain() for p in action.parameters]
             groundings[aname] = list(itertools.product(*domains))
@@ -155,3 +156,23 @@ def ground_symbols_exhaustively(symbols):
             variables.add(StateVariableLite(symbol, binding))
 
     return variables
+
+
+def ground_partially_grounded_terms_exhaustively(terms: List[CompoundTerm]) -> SymbolIndex:
+    """
+    Creates an index with all possible groundings for the given terms, taking into account arguments which have
+    been set to a constant
+    :param terms:
+    :return:
+    """
+    variables = SymbolIndex()
+
+    for term in terms:
+        domains = [s.domain() for s in term.symbol.domain]
+        for i in range(len(domains)):
+            if isinstance(term.subterms[i], Constant):
+                domains[i] = [term.subterms[i]]
+        for binding in itertools.product(*domains):
+            variables.add(StateVariableLite(term.symbol, binding))
+
+    return variables
diff --git a/src/tarski/io/_fstrips/common.py b/src/tarski/io/_fstrips/common.py
index dec17303..ab28ec0c 100644
--- a/src/tarski/io/_fstrips/common.py
+++ b/src/tarski/io/_fstrips/common.py
@@ -66,8 +66,7 @@ def get_requirements_string(problem):
 
 def create_number_type(lang):
     """ Creates a sort corresponding to the PDDL "number" """
-    # For the moment being, we assume that PDDL "number"s are unbound integers
-    parent = lang.get_sort("Integer")
+    parent = lang.get_sort("Real")
     lower = parent.lower_bound
     upper = parent.upper_bound
     lang.interval("number", parent, lower, upper)
@@ -135,7 +134,7 @@ def uniformize_costs(problem):
             action.cost = generate_zero_action_cost(action.language)
 
 
-class LowerCasingStreamWrapper(object):
+class LowerCasingStreamWrapper:
     """ A simple wrapper around a stream to lowercase all characters.
      @see https://github.com/antlr/antlr4/blob/master/doc/case-insensitive-lexing.md
     """
diff --git a/src/tarski/io/common.py b/src/tarski/io/common.py
index 35ff5cda..bb5fc050 100644
--- a/src/tarski/io/common.py
+++ b/src/tarski/io/common.py
@@ -5,5 +5,5 @@
 
 
 def load_tpl(name):
-    with open(os.path.join(_CURRENT_DIR_, "templates", name), 'r') as file:
+    with open(os.path.join(_CURRENT_DIR_, "templates", name), 'r', encoding='utf8') as file:
         return file.read()
diff --git a/src/tarski/io/fstrips.py b/src/tarski/io/fstrips.py
index 9d66f50e..5de5ed61 100644
--- a/src/tarski/io/fstrips.py
+++ b/src/tarski/io/fstrips.py
@@ -62,6 +62,14 @@ def parse_instance(self, filename):
         self.parse_file(filename, 'problem')
         return self.problem
 
+    def parse_domain_string(self, domain):
+        self.parse_string(domain, 'domain')
+        uniformize_costs(self.problem)
+
+    def parse_instance_string(self, instance):
+        self.parse_string(instance, 'problem')
+        return self.problem
+
     def parse_string(self, string, start_rule):
         logging.debug('Parsing custom string from grammar rule "{}"'.format(start_rule))
         parse_tree, _ = self.parser.parse_string(string, start_rule)
@@ -198,7 +206,7 @@ def print_domain(self, constant_objects: Optional[List[Constant]] = None):
         return content
 
     def write_domain(self, filename, constant_objects):
-        with open(filename, 'w') as file:
+        with open(filename, 'w', encoding='utf8') as file:
             file.write(self.print_domain(constant_objects))
 
     def print_instance(self, constant_objects: Optional[List[Constant]] = None):
@@ -229,7 +237,7 @@ def print_instance(self, constant_objects: Optional[List[Constant]] = None):
         return content
 
     def write_instance(self, filename, constant_objects):
-        with open(filename, 'w') as file:
+        with open(filename, 'w', encoding='utf8') as file:
             file.write(self.print_instance(constant_objects))
 
     def get_types(self):
@@ -239,10 +247,11 @@ def get_types(self):
                 continue  # Don't declare builtin elements
             tname = tarski_to_pddl_type(t)
             p = parent(t)
-            if p:
-                res.append("{} - {}".format(tname, tarski_to_pddl_type(p)))
-            else:
-                res.append(tname)
+            if p is not None:
+                tname = f"{tname} - {tarski_to_pddl_type(p)}"
+            res.append(tname)
+        # We always add the object type on the last line
+        res.append("object")
         return ("\n" + _TAB * 2).join(res)
 
     def get_functions(self):
@@ -329,7 +338,7 @@ def print_effects(effects, cost=None, indentation=0):
     if cost:  # Add the increase-effect corresponding to the action cost
         assert isinstance(cost, AdditiveActionCost)
         totalcost = cost.addend.language.get('total-cost')
-        effects.append(print_unconditional_effect(IncreaseEffect(totalcost(), cost.addend), indentation+1))
+        effects.append(print_unconditional_effect(IncreaseEffect(totalcost(), cost.addend), indentation + 1))
     return "(and\n{})".format("\n".join(effects))
 
 
diff --git a/src/tarski/io/pddl/__init__.py b/src/tarski/io/pddl/__init__.py
new file mode 100644
index 00000000..be91cfa4
--- /dev/null
+++ b/src/tarski/io/pddl/__init__.py
@@ -0,0 +1,45 @@
+# tarski: An AI Planning Modeling Framework
+#
+# Author: Miquel Ramirez (miquel.ramirez@pm.me) 2018-2022
+#
+#----------------------------------------------------------------------------------------------------------------------
+# io/pddl/__init__.py
+#
+# PDDL common definitions and data structures
+#----------------------------------------------------------------------------------------------------------------------
+
+from enum import Enum
+
+
+class Features(Enum):
+    """PDDL 3.1 features"""
+    STRIPS = 0,
+    TYPING = 1,
+    NEGATIVE_PRECONDITIONS = 2,
+    DISJUNCTIVE_PRECONDITIONS = 3,
+    EQUALITY = 4,
+    EXISTENTIAL_PRECONDITIONS = 5,
+    UNIVERSAL_PRECONDITIONS = 6,
+    CONDITIONAL_EFFECTS = 7,
+    NUMERIC_FLUENTS = 8,
+    DURATIVE_ACTIONS = 9,
+    DURATION_INEQUALITIES = 10,
+    CONTINUOUS_EFFECTS = 11,
+    DERIVED_PREDICATES = 12,
+    TIMED_INITIAL_LITERALS = 13,
+    PREFERENCES = 14,
+    CONSTRAINTS = 15,
+    ACTION_COSTS = 16,
+    OBJECT_FLUENTS = 17
+
+
+supported_features = {
+    Features.STRIPS,
+    Features.TYPING,
+    Features.NEGATIVE_PRECONDITIONS,
+    Features.EQUALITY,
+    Features.DURATIVE_ACTIONS,
+    Features.OBJECT_FLUENTS,
+    Features.DERIVED_PREDICATES,
+    Features.NUMERIC_FLUENTS
+}
diff --git a/src/tarski/io/pddl/errors.py b/src/tarski/io/pddl/errors.py
new file mode 100644
index 00000000..2eecbc52
--- /dev/null
+++ b/src/tarski/io/pddl/errors.py
@@ -0,0 +1,40 @@
+# tarski: An AI Planning Modeling Framework
+#
+# Author: Miquel Ramirez (miquel.ramirez@pm.me)
+# ----------------------------------------------------------------------------------------------------------------------
+# io/pddl/errors.py
+#
+# PDDL parser
+# ----------------------------------------------------------------------------------------------------------------------
+
+class SemanticError(Exception):
+
+    def __init__(self, line, msg):
+        super().__init__(msg)
+        self.line = line
+        self.msg = msg
+
+    def __str__(self):
+        return "Semantic Error: Line {}: {}".format(self.line, self.msg)
+
+
+class ParseError(Exception):
+
+    def __init__(self, line, msg):
+        super().__init__(msg)
+        self.line = line
+        self.msg = msg
+
+    def __str__(self):
+        return "Parse Error: Line {}: {}".format(self.line, self.msg)
+
+
+class UnsupportedFeature(Exception):
+
+    def __init__(self, line, msg):
+        super().__init__(msg)
+        self.line = line
+        self.msg = msg
+
+    def __str__(self):
+        return "Unsupported PDDL feature: Line {}: {}".format(self.line, self.msg)
diff --git a/src/tarski/io/pddl/instance.py b/src/tarski/io/pddl/instance.py
new file mode 100644
index 00000000..b31ad51a
--- /dev/null
+++ b/src/tarski/io/pddl/instance.py
@@ -0,0 +1,371 @@
+# tarski: An AI Planning Modeling Framework
+#
+# Author: Miquel Ramirez (miquel.ramirez@pm.me)
+# ----------------------------------------------------------------------------------------------------------------------
+# io/pddl/instance.py
+#
+# PDDL parser
+# ----------------------------------------------------------------------------------------------------------------------
+
+from collections import namedtuple, OrderedDict
+from typing import Tuple
+from enum import Enum
+
+import tarski as tsk
+from tarski.io.pddl.errors import UnsupportedFeature
+from tarski.theories import Theory
+from tarski.syntax import Variable, Sort
+from tarski.syntax.sorts import Interval, int_encode_fn
+from tarski.syntax import symref
+
+
+AssignmentEffectData = namedtuple('AssignmentEffectData', ['lhs', 'rhs'])
+EventData = namedtuple('EventData', ['pre', 'post'])
+ActionData = namedtuple('ActionData', ['name', 'parameters', 'pre', 'post'])
+DurativeActionData = namedtuple('DurativeActionData', ['name', 'parameters', 'at_start', 'at_end', 'overall', 'duration'])
+DerivedPredicateData = namedtuple('DerivedPredicateData', ['head', 'parameters', 'body'])
+ObjectiveData = namedtuple('ObjectiveData', ['mode', 'type', 'expr'])
+
+
+class ObjectiveMode(Enum):
+    """
+    Enumeration with mode of optimization
+    """
+    MINIMIZE = 0
+    MAXIMIZE = 1
+
+
+class ObjectiveType(Enum):
+    """
+    Enumeration of all possible types of objectives
+    """
+    UNSPECIFIED = 0
+    TOTAL_COST = 1
+    TOTAL_TIME = 2
+    FLUENT_EXPR = 3
+    VIOLATED_PREFS = 4
+
+
+class InstanceModel:
+    """
+    Instance objects represent the structural components of a planning problem instance as defined in a PDDL program.
+    The instance class is meant to be generic by not committing to specific further post-processing (such as
+    grounding).
+    """
+    def __init__(self, **kwargs):
+        self.debug = kwargs.get('debug', False)
+        self.L = None
+        self.bool_t = None
+        self.int_t = None
+        self.real_t = None
+        self.default_numeric_type = None
+
+        self._types = OrderedDict()
+        self._constants = OrderedDict()
+        self._predicates = OrderedDict()
+        self._functions = OrderedDict()
+
+        self._actions = []
+        self._durative = []
+        self._derived = []
+
+        self.init = None
+        self.goal = None
+        self._objective = ObjectiveData(mode=ObjectiveMode.MINIMIZE, type=ObjectiveType.UNSPECIFIED, expr=None)
+
+        self.L = tsk.language('pddl-theory', theories=[Theory.EQUALITY, Theory.ARITHMETIC])
+
+        self.bool_t = Interval('Bool', self.L, int_encode_fn, 0, 1, builtin=True)
+        self.int_t = self.L.Integer
+        self.real_t = self.L.Real
+        self.L.attach_sort(self.bool_t, self.int_t)
+        # MRJ: this is prone to change
+        self.default_numeric_type = self.int_t
+
+        self._types['object'] = self.L.Object
+
+
+    @property
+    def lang(self):
+        """
+        The `tarski.FirstOrderLanguage` object representing the domain theory given for the instance
+        :return:
+        """
+        return self.L
+
+    @property
+    def types(self):
+        """
+        Dictionary of types defined in the PDDL program
+        :return:
+        """
+        return self._types
+
+    @property
+    def domains(self):
+        """
+        Dictionary of constants defined in the PDDL program
+        :return:
+        """
+        return self._constants
+
+    @property
+    def predicates(self):
+        """
+        Dictionary of predicates defined in the PDDL program
+        :return:
+        """
+        return self._predicates
+
+    @property
+    def functions(self):
+        """
+        Dictionary of functions defined in the PDDL program
+        :return:
+        """
+        return self._functions
+
+    @property
+    def actions(self):
+        """
+        List object with `ActionData` instances describing the elements in the definition of (instantaneous) actions
+        :return:
+        """
+        return self._actions
+
+    @property
+    def durative(self):
+        """
+        List of objects with `DurativeActionData` instances describing durative actions
+        :return:
+        """
+        return self._durative
+
+    @property
+    def derived(self):
+        """
+        List of objects with `DerivedPredicateData` instances describing derived predicates (PDDL axioms)
+        :return:
+        """
+        return self._derived
+
+    @property
+    def objective(self):
+        """
+        Objective function
+        :return:
+        """
+        return self._objective
+
+    def process_supertype_definition(self, supertype, subtypes, lineno):
+        """
+        Processes a super type definition (e.g. a PDDL type with subtypes)
+        :param supertype:
+        :param subtypes:
+        :param lineno: line number, used to generate errors
+        :return:
+        """
+        added_supertype = False
+        if supertype not in self.types:
+            self.types[supertype] = self.L.sort(supertype, self.L.Object)
+            added_supertype = True
+
+        for subtypename in subtypes:
+            if subtypename in self.types:
+                if added_supertype:
+                    self.L.set_parent(self.types[subtypename], self.types[supertype], overwrite=True)
+                else:
+                    msg = "Error processing types definition in domain: type '{}' defined via multiple inheritance, which is not supported".format(
+                        subtypename)
+                    raise UnsupportedFeature(lineno(), msg)
+            else:
+                self.types[subtypename] = self.L.sort(subtypename, self.types[supertype])
+
+    def process_type_definition(self, type_data):
+        """
+        Processes a type definition
+        :param type_data:
+        :return:
+        """
+        self.types[type_data] = self.L.sort(type_data, self.L.Object)
+
+    def process_constant_definition(self, constant_data):
+        """
+        Processes the definition of a constant
+        :param constant_data:
+        :return:
+        """
+        typename, constant_list = constant_data
+        if typename in self.domains:
+            self.domains[typename] += [self.L.constant(c, self.types[typename]) for c in constant_list]
+            return
+
+        self.domains[typename] = [self.L.constant(c, self.types[typename]) for c in constant_list]
+
+    def process_predicate_definition(self, predicate_data):
+        """
+        Processes a predicate definition
+        :param predicate_data:
+        :return:
+        """
+        name, args = predicate_data
+        signature = tuple([a['type'] for a in args] + [self.bool_t])
+        self.predicates[name] = self.L.function(name, *signature)
+
+    def get_variable(self, name: str, typename: str) -> Tuple[Variable, Sort]:
+        """
+        Returns a term (variable) and its sort for a given variable name and sort
+        :param name:
+        :return:
+        """
+        return self.L.variable(name, typename), self.L.get(typename)
+
+    def get(self, stuff: str):
+        """
+        Checks if stuff is defined in the domain theory of the instance
+        :param stuff:
+        :return:
+        """
+        return self.L.get(stuff)
+
+    def int_const(self, v: str):
+        """
+        Returns an Integer constant
+        :param v:
+        :return:
+        """
+        return self.L.constant(v, self.int_t)
+
+    def real_const(self, v: str):
+        """
+        Returns an Integer constant
+        :param v:
+        :return:
+        """
+        return self.L.constant(v, self.real_t)
+
+    def process_function_skeleton(self, func_data):
+        """
+        Processes function definition
+        :param func_data:
+        :return:
+        """
+        name = func_data['name']
+        domain = [a['type'] for a in func_data['domain']]
+        try:
+            codomain = func_data['codomain']
+        except KeyError:
+            codomain = self.default_numeric_type
+        signature = domain + [codomain]
+        self.functions[name] = self.L.function(name, *signature)
+
+    def process_durative_action_skeleton(self, name, parameters, body):
+        """
+        Processes durative action skeleton
+        :param name:
+        :param parameters:
+        :param body:
+        :return:
+        """
+
+        at_start_formulas = []
+        at_end_formulas = []
+        over_all_formulas = []
+        for entry in body['precondition']:
+            if entry['type'] == 'instant':
+                if entry['offset'] == 'start':
+                    phi = entry['formula']
+                    phi_ref = symref(phi)
+                    if phi_ref not in at_start_formulas:
+                        at_start_formulas += [phi_ref]
+                elif entry['offset'] == 'end':
+                    phi_ref = symref(entry['formula'])
+                    if phi_ref not in at_end_formulas:
+                        at_end_formulas += [phi_ref]
+            elif entry['type'] == 'interval':
+                phi_ref = symref(entry['formula'])
+                if phi_ref not in over_all_formulas:
+                    over_all_formulas += [phi_ref]
+
+        if self.debug:
+            print("At start preconditions:", len(at_start_formulas))
+            print("At end preconditions:", len(at_end_formulas))
+            print("Over all conditions:", len(over_all_formulas))
+
+        duration = body['duration']
+
+        at_start_eff = []
+        at_end_eff = []
+
+        for eff_entry in body['effect']:
+            if eff_entry['instant'] == 'end':
+                at_end_eff += [eff_entry['effect'][0]]
+                continue
+            at_start_eff += [eff_entry['effect'][0]]
+
+        self._durative += [DurativeActionData(name=name,
+                                              parameters=[e['term'] for e in parameters],
+                                              duration=duration,
+                                              at_start=EventData(pre=at_start_formulas, post=at_start_eff),
+                                              at_end=EventData(pre=at_end_formulas, post=at_end_eff),
+                                              overall=over_all_formulas)]
+
+    def process_instantaneous_action_skeleton(self, name, parameters, body):
+        """
+        Processes instantaneous action skeleton
+        :param name:
+        :param parameters:
+        :param body:
+        :return:
+        """
+
+        self._actions += [ActionData(name=name,
+                                     parameters=[e['term'] for e in parameters],
+                                     pre=body['precondition'],
+                                     post=body['effect'])]
+
+    def process_action_skeleton(self, name, parameters, body):
+        """
+        Processes parsed action skeleton
+        :param name:
+        :param parameters:
+        :param body:
+        :return:
+        """
+        if self.debug:
+            print("Action:", name)
+            print("parameters:", parameters)
+            print("data:", body)
+            print("precondition:", body['precondition'])
+            print("effect:", body['effect'])
+
+        if 'duration' in body:
+            self.process_durative_action_skeleton(name, parameters, body)
+            return
+
+        self.process_instantaneous_action_skeleton(name, parameters, body)
+
+    def process_derived_predicate_skeleton(self, head, parameters, body):
+        """
+        Processes skeleton of derived predicate
+        """
+        if self.debug:
+            print("Derived predicate: head:", head)
+            print("Parameters:", parameters)
+            print("body:", body)
+
+        self._derived += [DerivedPredicateData(head=head,
+                                               parameters=[e['term'] for e in parameters],
+                                               body=body)]
+
+    def process_objective_definition(self, objective_data):
+        """
+        Processes the objective definition
+        :param objective_data:
+        :return:
+        """
+        self._objective = ObjectiveData(mode=objective_data['mode'],
+                                        type=objective_data['definition']['type'],
+                                        expr=objective_data['definition']['expr'])
+        if self.debug:
+            print("Objective: mode: {} type: {} expr: {}".format(self.objective.mode, self.objective.type, self.objective.expr))
diff --git a/src/tarski/io/pddl/lexer.py b/src/tarski/io/pddl/lexer.py
new file mode 100644
index 00000000..b4d7e0c2
--- /dev/null
+++ b/src/tarski/io/pddl/lexer.py
@@ -0,0 +1,268 @@
+# tarski
+#
+# Author: Miquel Ramirez (miquel.ramirez@pm.me)
+# ----------------------------------------------------------------------------------------------------------------------
+# io/pddl/lexer.py
+#
+# PDDL tokenizer
+# ----------------------------------------------------------------------------------------------------------------------
+
+import ply.lex as lex  # type: ignore
+import re
+
+# helper definitions
+alpha = r'[A-Za-z]'
+num = r'[0-9]'
+alpnum = r'[0-9A-Za-z_\']'
+identifier = r'(' + alpha + r')((' + alpha + r'|' + num + r'|\-|\_)*(' + alpha + r'|' + num + r'))?'
+variable = r'\?' + identifier
+directive = r':' + identifier
+section = r':\(' + identifier
+nat = num + r'+'
+real = num + r'*\.' + num + r'+'
+ws = ' \t'
+
+
+class LexSymbols:
+
+    def __init__(self):
+        # PDDL Requirements
+
+        self.rwSTRIPS = ':strips'
+        self.rwTYPING = ':typing'
+        self.rwNEGATIVE_PRECONDITIONS = ':negative-preconditions'
+        self.rwDISJUNCTIVE_PRECONDITIONS = ':disjunctive-preconditions'
+        self.rwEQUALITY = ':equality'
+        self.rwEXISTENTIAL_PRECONDITIONS = ':existential-preconditions'
+        self.rwUNIVERSAL_PRECONDITIONS = ':universal-preconditions'
+        self.rwQUANTIFIED_PRECONDITIONS = ':quantified-preconditions'
+        self.rwCONDITIONAL_EFFECTS = ':conditional-effects'
+        self.rwOBJECT_FLUENTS = ':object-fluents'
+        self.rwNUMERIC_FLUENTS = ':numeric-fluents'
+        self.rwFLUENTS = ':fluents'
+        self.rwADL = ':adl'
+        self.rwDURATIVE_ACTIONS = ':durative-actions'
+        self.rwDERIVED_PREDICATES = ':derived-predicates'
+        self.rwTIMED_INITAL_LITERALS = ':timed-initial-literals'
+        self.rwPREFERENCES = ':preferences'
+        self.rwACTION_COSTS = ':action-costs'
+        self.rwCONSTRAINTS = ':constraints'
+        self.rwCONTINUOUS_EFFECTS = ':continuous-effects'
+        self.rwDURATION_INEQUALITIES = ':duration-inequalities'
+        self.rwTIMED_INITIAL_LITERALS = ':timed-initial-literals'
+
+        # PDDL section tags
+
+        self.rwREQUIREMENTS = ':requirements'
+        self.rwINIT = ':init'
+        self.rwGOAL = ':goal'
+        self.rwACTION = ':action'
+        self.rwDOMAIN_REF = ':domain'
+        self.rwCONSTANTS = ':constants'
+        self.rwPREDICATES = ':predicates'
+        self.rwFUNCTIONS = ':functions'
+        self.rwCONSTRAINTS = ':constraints'
+        self.rwDERIVED = ':derived'
+        self.rwOBJECTS = ':objects'
+        self.rwMETRIC = ':metric'
+        self.rwDURATIVE_ACTION = ':durative-action'
+        self.rwTYPES = ':types'
+        self.rwDERIVED = ':derived'
+
+
+        # PDDL fields
+        self.rwPRECONDITION = ':precondition'
+        self.rwEFFECT = ':effect'
+        self.rwPARAMETERS = ':parameters'
+        self.rwCONDITION = ':condition'
+        self.rwDURATION = ':duration'
+
+
+        # PDDL keywords and operators
+        self.rwAND = 'and'
+        self.rwNOT = 'not'
+        self.rwOR = 'or'
+        self.rwIMPLY = 'imply'
+        self.rwEXISTS = 'exists'
+        self.rwFORALL = 'forall'
+        self.rwWHEN = 'when'
+        self.rwDOMAIN = 'domain'
+        self.rwPROBLEM = 'problem'
+        self.rwAT = 'at'
+        self.rwOVER = 'over'
+        self.rwSTART = 'start'
+        self.rwEND = 'end'
+        self.rwOVERALL = 'overall'
+        self.rwASSIGN = 'assign'
+        self.rwDEFINE = 'define'
+        self.rwINCREASE = 'increase'
+        self.rwDECREASE = 'decrease'
+        self.rwSCALE_UP = 'scale-up'
+        self.rwSCALE_DOWN = 'scale-down'
+        self.rwINT = 'int'
+        self.rwFLOAT = 'float'
+        self.rwOBJECT = 'object'
+        self.rwNUMBER = 'number'
+        self.rwEITHER = 'either'
+        self.rwALWAYS = 'always'
+        self.rwSOMETIME = 'sometime'
+        self.rwWITHIN = 'within'
+        self.rwAT_MOST_ONCE = 'at-most-once'
+        self.rwSOMETIME_AFTER = 'sometime-after'
+        self.rwSOMETIME_BEFORE = 'sometime-before'
+        self.rwALWAYS_WITHIN = 'always-within'
+        self.rwHOLD_DURING = 'hold-during'
+        self.rwHOLD_AFTER = 'hold-after'
+        self.rwPREFERENCE = 'preference'
+        self.rwUNDEFINED = 'undefined'
+        self.rwMINIMIZE = 'minimize'
+        self.rwMAXIMIZE = 'maximize'
+        self.rwDT = '#t'
+        self.rwTOTAL_TIME = 'total-time'
+        self.rwTOTAL_COST = 'total-cost'
+        self.rwIS_VIOLATED = 'is-violated'
+        self.rwALL = 'all'
+
+
+class PDDLlex:
+    """
+    Lexical analyser for PDDL
+    """
+
+    def __init__(self):
+        self._lexer = None
+        self.symbols = LexSymbols()
+
+        self.reserved = {getattr(self.symbols, attr): attr for attr in self.symbols.__dict__.keys() if 'rw' in attr}
+
+        self.tokens = [
+            'LPAREN',
+            'RPAREN',
+            'LBRACE',
+            'RBRACE',
+            'LBRACKET',
+            'RBRACKET',
+            'DOUBLEDOT',
+            'COMMA',
+            'ASSIGN',
+            'SEMICOLON',
+            'ARROW',
+            'EQUA',
+            'NEQUA',
+            'LT',
+            'GT',
+            'LEQ',
+            'GEQ',
+            'PLUS',
+            'MINUS',
+            'TIMES',
+            'DIV',
+            'ID',
+            'VAR',
+            'NAT',
+            'REAL',
+            'QUESTION',
+            'UNKNOWN_DIRECTIVE',
+            'UNKNOWN_SECTION'
+        ]
+        self.tokens += list(self.reserved.values())
+
+        self.regexp = {
+            'ID': re.compile(identifier),
+            'VAR': re.compile(variable),
+            'NAT': re.compile(nat),
+            'REAL': re.compile(real)
+        }
+
+    t_ignore = ws
+
+    t_QUESTION = r'\?'
+    t_LPAREN = r'\('
+    t_RPAREN = r'\)'
+    #t_LBRACE = r'\{'
+    #t_RBRACE = r'\}'
+    #t_LBRACKET = r'\['
+    #t_RBRACKET = r'\]'
+    t_DOUBLEDOT = r'\.\.'
+    #t_COMMA = r','
+    #t_ASSIGN = r':='
+    #t_SEMICOLON = r';'
+    t_EQUA = r'='
+    #t_NEQUA = r'!='
+    t_LT = r'<'
+    t_GT = r'>'
+    t_LEQ = r'<='
+    t_GEQ = r'>='
+    t_PLUS = r'\+'
+    t_MINUS = r'-'
+    t_TIMES = r'\*'
+    t_DIV = r'/'
+
+    def t_newline(self, t):
+        r'\n+'
+        self._lexer.lineno += len(t.value)
+
+    def t_ignore_COMMENT(self, t):
+        r';.*'
+
+    @lex.TOKEN(identifier)
+    def t_ID(self, t):
+        t.type = self.reserved.get(t.value, 'ID')
+        t.value = t.value.lower()
+        return t
+
+    @lex.TOKEN(variable)
+    def t_VAR(self, t):
+        t.type = self.reserved.get(t.value, 'VAR')
+        return t
+
+    @lex.TOKEN(directive)
+    def t_DIRECTIVE(self, t):
+        t.type = self.reserved.get(t.value, 'UNKNOWN_DIRECTIVE')
+        return t
+
+    @lex.TOKEN(section)
+    def t_SECTION(self, t):
+        t.type = self.reserved.get(t.value, 'UNKNOWN_SECTION')
+
+    @lex.TOKEN(real)
+    def t_REAL(self, t):
+        t.value = float(t.value)
+        return t
+
+    @lex.TOKEN(nat)
+    def t_NAT(self, t):
+        t.value = int(t.value)
+        return t
+
+    def t_error(self, t):
+        print("Illegal character: {} at line: {}".format(repr(t.value[0]), self._lexer.lineno))
+        t.lexer.skip(1)
+
+    def lineno(self):
+        return self._lexer.lineno
+
+    def build(self, **kwargs):
+        self._lexer = lex.lex(object=self, **kwargs)
+
+    def token(self):
+        return self._lexer.token()
+
+    def input(self, data):
+        if self._lexer is None:
+            self.build()
+        self._lexer.input(data)
+
+    def is_identifier(self, v):
+        return self.regexp['ID'].match(v) is not None
+
+    def is_variable(self, v: str):
+        #print("is variable:", v, type(v))
+        return self.regexp['VAR'].match(v) is not None
+
+    def __call__(self):
+        while True:
+            tok = self.token()
+            if not tok:
+                break
+            yield tok
\ No newline at end of file
diff --git a/src/tarski/io/pddl/parser.py b/src/tarski/io/pddl/parser.py
new file mode 100644
index 00000000..528f4e16
--- /dev/null
+++ b/src/tarski/io/pddl/parser.py
@@ -0,0 +1,1453 @@
+# tarski: An AI Planning Modeling Framework
+#
+# Author: Miquel Ramirez (miquel.ramirez@pm.me)
+# ----------------------------------------------------------------------------------------------------------------------
+# io/pddl/parser.py
+#
+# PDDL parser
+# ----------------------------------------------------------------------------------------------------------------------
+
+import logging
+
+from ply import yacc  # type: ignore
+
+from tarski.syntax import CompoundTerm, Term, land, lor, neg, QuantifiedFormula, Quantifier
+from tarski.io.pddl import Features, supported_features
+from tarski.io.pddl.lexer import PDDLlex
+from tarski.io.pddl.instance import *
+from tarski.io.pddl.errors import *
+
+
+class PDDLparser:
+    """
+    The PDDL parser class
+    """
+    def __init__(self, lexer=None, verbose=False, debug=False):
+        if lexer is None:
+            self.lexer = PDDLlex()
+            # Lexer debugging isn't particularly helpful, change
+            # line below if this assessment ever changes
+            self.lexer.build(debug=False)
+        else:
+            self.lexer = lexer
+
+        self.logfile = None
+        self.verbose = verbose
+        self.tokens = self.lexer.tokens
+
+        self.domain_name = None
+        self.problem_name = None
+        self.required_features = set()
+
+        self.precedence = (
+            ('left', 'rwIMPLY'),
+            ('left', 'rwOR'),
+            ('left', 'rwAND'),
+            ('nonassoc', 'rwNOT'),
+            ('nonassoc', 'LT', 'GT', 'LEQ', 'GEQ', 'EQUA', 'NEQUA'),
+            ('left', 'MINUS', 'PLUS'),
+            ('left', 'TIMES', 'DIV')
+        )
+
+        self.debug = debug
+        self.instance = None
+        self.var_dict = None
+        self._parser = None
+
+    def build(self, **kwargs):
+        # self.logfile = os.path.join(tempfile.gettempdir(), 'pddl_parse.log')
+        self.logfile = kwargs.get('logfile', 'pddl_parse.log')
+        self.log = logging.getLogger(__name__)
+        self.log.addHandler(logging.FileHandler(self.logfile, mode='w'))
+        self._parser = yacc.yacc(module=self, start='begin', debug=self.debug)
+        self.instance = InstanceModel(debug=self.debug)
+        self.var_dict = OrderedDict()
+
+    def parse(self, input_data):
+        return self._parser.parse(input=input_data, lexer=self.lexer, debug=self.log)
+
+    def _print_verbose(self, p_name):
+        if self.verbose:
+            print('>> Parsed `{}` ...'.format(p_name))
+
+    def p_begin(self, p):
+        '''begin    : domain
+                    | domain problem
+                    | problem'''
+        pass
+
+    def p_domain(self, p):
+        '''domain   : LPAREN rwDEFINE domain_name domain_require_def domain_body_def RPAREN'''
+        print("Domain Parsed")
+        pass
+
+    # Rules like A : B C D
+    # where B C and D are like: X : Y | empty
+    # can't work because of parser disambiguation skipping possible productions
+    # excellent explanation of the issue here: https://stackoverflow.com/a/61966032/338107
+    def p_domain_body_def(self, p):
+        '''domain_body_def  : types_def domain_body_def
+                            | constants_def domain_body_def
+                            | predicates_def domain_body_def
+                            | functions_def domain_body_def
+                            | domain_constraints_def domain_body_def
+                            | structure_def_list'''
+        pass
+
+    def p_domain_name(self, p):
+        '''domain_name : LPAREN rwDOMAIN  ID RPAREN'''
+        self.domain_name = p[3]
+
+    def p_problem(self, p):
+        '''problem  : LPAREN rwDEFINE problem_name domain_ref problem_body_def RPAREN'''
+        pass
+
+    def p_problem_body_def(self, p):
+        '''
+        problem_body_def    :  problem_require_def problem_body_def
+                            | object_declaration problem_body_def
+                            | init problem_body_def
+                            | goal problem_body_def
+                            | problem_constraints_def problem_body_def
+                            | metric_spec
+                            | empty
+        '''
+        pass
+
+    def p_problem_name(self, p):
+        '''problem_name    : LPAREN rwPROBLEM ID RPAREN'''
+        self.problem_name = p[3]
+
+    def p_domain_ref(self, p):
+        '''domain_ref  : LPAREN rwDOMAIN_REF ID RPAREN'''
+        expected_domain = p[3]
+        if expected_domain != self.domain_name:
+            msg = "Domain and problem mismatch: expected domain name is '{}', provided domain is '{}'".format(expected_domain, self.domain_name)
+            raise SemanticError(self.lexer.lineno(), msg)
+
+    def p_domain_require_def(self, p):
+        '''domain_require_def  :  LPAREN rwREQUIREMENTS requirement_key_list RPAREN'''
+        self.check_requirements_support()
+
+    def check_requirements_support(self):
+        """
+        Checks if requirements are supported
+        :return:
+        """
+        unsupported = self.required_features - supported_features
+        if len(unsupported) > 0:
+            msg = "The following UNSUPPORTED features are required: {}".format(" ".join([f.name for f in unsupported]))
+            raise UnsupportedFeature(self.lexer.lineno(), msg)
+
+    def p_requirement_key_list(self, p):
+        '''requirement_key_list : rwSTRIPS requirement_key_list
+                                | rwTYPING requirement_key_list
+                                | rwNEGATIVE_PRECONDITIONS requirement_key_list
+                                | rwDISJUNCTIVE_PRECONDITIONS requirement_key_list
+                                | rwEQUALITY requirement_key_list
+                                | rwEXISTENTIAL_PRECONDITIONS requirement_key_list
+                                | rwUNIVERSAL_PRECONDITIONS requirement_key_list
+                                | rwQUANTIFIED_PRECONDITIONS requirement_key_list
+                                | rwCONDITIONAL_EFFECTS requirement_key_list
+                                | rwFLUENTS requirement_key_list
+                                | rwNUMERIC_FLUENTS requirement_key_list
+                                | rwADL requirement_key_list
+                                | rwDURATIVE_ACTIONS requirement_key_list
+                                | rwDURATION_INEQUALITIES requirement_key_list
+                                | rwCONTINUOUS_EFFECTS requirement_key_list
+                                | rwDERIVED_PREDICATES requirement_key_list
+                                | rwTIMED_INITIAL_LITERALS requirement_key_list
+                                | rwPREFERENCES requirement_key_list
+                                | rwCONSTRAINTS requirement_key_list
+                                | rwACTION_COSTS requirement_key_list
+                                | empty'''
+        # If token 1 is null, we have hit the end of the list
+        if p[1] is None:
+            return
+        if p[1] == self.lexer.symbols.rwSTRIPS:
+            self.required_features.add(Features.STRIPS)
+        elif p[1] == self.lexer.symbols.rwTYPING:
+            self.required_features.add(Features.TYPING)
+        elif p[1] == self.lexer.symbols.rwNEGATIVE_PRECONDITIONS:
+            self.required_features.add(Features.NEGATIVE_PRECONDITIONS)
+        elif p[1] == self.lexer.symbols.rwDISJUNCTIVE_PRECONDITIONS:
+            self.required_features.add(Features.DISJUNCTIVE_PRECONDITIONS)
+        elif p[1] == self.lexer.symbols.rwEQUALITY:
+            self.required_features.add(Features.EQUALITY)
+        elif p[1] == self.lexer.symbols.rwEXISTENTIAL_PRECONDITIONS:
+            self.required_features.add(Features.EXISTENTIAL_PRECONDITIONS)
+        elif p[1] == self.lexer.symbols.rwUNIVERSAL_PRECONDITIONS:
+            self.required_features.add(Features.UNIVERSAL_PRECONDITIONS)
+        elif p[1] == self.lexer.symbols.rwQUANTIFIED_PRECONDITIONS:
+            self.required_features.add(Features.UNIVERSAL_PRECONDITIONS)
+            self.required_features.add(Features.EXISTENTIAL_PRECONDITIONS)
+        elif p[1] == self.lexer.symbols.rwCONDITIONAL_EFFECTS:
+            self.required_features.add(Features.CONDITIONAL_EFFECTS)
+        elif p[1] == self.lexer.symbols.rwOBJECT_FLUENTS:
+            self.required_features.add(Features.OBJECT_FLUENTS)
+        elif p[1] == self.lexer.symbols.rwNUMERIC_FLUENTS:
+            self.required_features.add(Features.NUMERIC_FLUENTS)
+        elif p[1] == self.lexer.symbols.rwFLUENTS:
+            self.required_features.add(Features.OBJECT_FLUENTS)
+            self.required_features.add(Features.NUMERIC_FLUENTS)
+        elif p[1] == self.lexer.symbols.rwADL:
+            self.required_features.add(Features.STRIPS)
+            self.required_features.add(Features.TYPING)
+            self.required_features.add(Features.NEGATIVE_PRECONDITIONS)
+            self.required_features.add(Features.DISJUNCTIVE_PRECONDITIONS)
+            self.required_features.add(Features.EQUALITY)
+            self.required_features.add(Features.UNIVERSAL_PRECONDITIONS)
+            self.required_features.add(Features.EXISTENTIAL_PRECONDITIONS)
+            self.required_features.add(Features.CONDITIONAL_EFFECTS)
+        elif p[1] == self.lexer.symbols.rwDURATIVE_ACTIONS:
+            self.required_features.add(Features.DURATIVE_ACTIONS)
+        elif p[1] == self.lexer.symbols.rwDERIVED_PREDICATES:
+            self.required_features.add(Features.DERIVED_PREDICATES)
+        elif p[1] == self.lexer.symbols.rwTIMED_INITAL_LITERALS:
+            self.required_features.add(Features.TIMED_INITIAL_LITERALS)
+        elif p[1] == self.lexer.symbols.rwPREFERENCES:
+            self.required_features.add(Features.PREFERENCES)
+        elif p[1] == self.lexer.symbols.rwCONSTRAINTS:
+            self.required_features.add(Features.CONSTRAINTS)
+        elif p[1] == self.lexer.symbols.rwACTION_COSTS:
+            self.required_features.add(Features.ACTION_COSTS)
+        elif p[1] == self.lexer.symbols.rwCONTINUOUS_EFFECTS:
+            self.required_features.add(Features.CONTINUOUS_EFFECTS)
+        elif p[1] == self.lexer.symbols.rwTIMED_INITAL_LITERALS:
+            self.required_features.add(Features.DURATIVE_ACTIONS)
+            self.required_features.add(Features.TIMED_INITIAL_LITERALS)
+        elif p[1] == self.lexer.symbols.rwDURATION_INEQUALITIES:
+            self.required_features.add(Features.DURATIVE_ACTIONS)
+            self.required_features.add(Features.DURATION_INEQUALITIES)
+        else:
+            raise RuntimeError("Handling of requirements should be exhaustive!")
+
+    def p_types_def(self, p):
+        '''types_def    : LPAREN rwTYPES typed_list_of_names RPAREN'''
+        if p[1] is None:
+            return
+        if self.debug:
+            print("Defined types:", p[3])
+        for type_entry in p[3]:
+            if isinstance(type_entry, tuple):
+                supertype, subtypes = type_entry
+                self.instance.process_supertype_definition(supertype, subtypes, self.lexer.lineno())
+            else:
+                self.instance.process_type_definition(type_entry)
+
+    def p_typed_list_of_names(self, p):
+        '''typed_list_of_names  : ID list_of_names typed_list_of_names
+                                | ID list_of_names MINUS type typed_list_of_names
+                                | type typed_list_of_names
+                                | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        if len(p) == 4:
+            # 1st production rule
+            new_name = p[1]
+            p[0] = [new_name] + p[2]
+            return
+        if len(p) == 6:
+            sub_types = [p[1]] + p[2]
+            super_type = p[4]
+            p[0] = [(super_type, sub_types)] + p[5]
+            return
+        p[0] = [p[1]] + p[2]
+
+    def p_list_of_names(self, p):
+        '''list_of_names    : ID list_of_names
+                            | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        p[0] = [p[1]] + p[2]
+
+    def p_type(self, p):
+        '''type : LPAREN rwEITHER primitive_type_list RPAREN
+                | primitive_type'''
+        if len(p) > 2:
+            assert p[2] == self.lexer.symbols.rwEITHER
+            msg = "Unsupported PDDL feature: (either <list of type names>)"
+            raise SemanticError(self.lexer.lineno(), msg)
+        p[0] = p[1]
+
+    def p_primitive_type_list(self, p):
+        '''primitive_type_list  : primitive_type primitive_type_list
+                                | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        p[0] = [p[1]] + p[2]
+
+    def p_primitive_type(self, p):
+        '''primitive_type   : rwOBJECT
+                            | rwNUMBER
+                            | ID'''
+        if p[1] == self.lexer.symbols.rwOBJECT:
+            p[0] = 'Object'
+            return
+        if p[1] == self.lexer.symbols.rwNUMBER:
+            p[0] = 'Real'
+            return
+        p[0] = p[1]
+
+    def p_constants_def(self, p):
+        '''constants_def    :  LPAREN rwCONSTANTS typed_list_of_names RPAREN'''
+        if p[1] is None:
+            if self.debug:
+                print("No constants defined")
+            return
+        def_list = p[3]
+        total_constants = 0
+        for entry in def_list:
+            if isinstance(entry, tuple):
+                typename, constant_list = entry
+                if typename not in self.instance.types:
+                    msg = "Error parsing (:constants ) section: type '{}' was not defined".format(typename)
+                    raise SemanticError(self.lexer.lineno(), msg)
+
+                self.instance.process_constant_definition(entry)
+
+                total_constants += len(constant_list)
+            else:
+                msg = "Error processing (:constants ) section: constant '{}' has no type attached".format(entry)
+                raise SemanticError(self.lexer.lineno(), msg)
+        if self.debug:
+            print("Total constants defined:", total_constants)
+
+
+    def p_predicates_def(self, p):
+        '''predicates_def   : LPAREN rwPREDICATES list_of_atomic_formula_skeleton RPAREN
+                            | LPAREN rwPREDICATES RPAREN'''
+        if p[1] is None:
+            return
+        if len(p) == 4:
+            return
+        def_pred_list = p[3]
+        if self.debug:
+            print("Number of predicates defined:", len(def_pred_list))
+        for _, name, args in def_pred_list:
+            self.instance.process_predicate_definition((name, args))
+        if self.debug:
+            for k, f in self.instance.predicates.items():
+                print("\t", f)
+
+
+    def p_list_of_atomic_formula_skeleton(self, p):
+        '''list_of_atomic_formula_skeleton  : atomic_formula_skeleton list_of_atomic_formula_skeleton
+                                            | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        p[0] = [p[1]] + p[2]
+
+    def normalize_typed_variable_list(self, unnorm_args):
+        """
+        Normalizes a list of typed variables
+        """
+        unnorm_args += [('type', 'object')]
+        args = []
+        last_index = -1
+        for i, token in enumerate(unnorm_args):
+            token_type, token_value = token
+            if token_type == 'type':
+                var_type = token_value
+                if var_type not in self.instance.types:
+                    msg = "Error parsing list of typed variables: type '{}' is not defined".format(var_type)
+                    raise SemanticError(self.lexer.lineno(), msg)
+                for t2 in unnorm_args[last_index+1:i]:
+                    var_term, var_sort = self.instance.get_variable(t2[1], var_type)
+                    args += [{'var': t2[1], 'term': var_term, 'type': var_sort}]
+                last_index = i
+        return args
+
+    def p_atomic_formula_skeleton(self, p):
+        '''atomic_formula_skeleton  : LPAREN predicate list_of_typed_variables RPAREN'''
+        # MRJ: rule #2 is for untyped PDDL, where object is the assumed type
+
+        if self.debug:
+            print("Defined predicate:", p[2])
+        # Normalize arguments
+        p[0] = ('atomic_formula_skeleton', p[2], self.normalize_typed_variable_list(p[3]))
+
+    def p_atomic_formula_skeleton_vars(self, p):
+        '''atomic_formula_skeleton_vars  : LPAREN predicate list_of_typed_variables RPAREN'''
+        # Normalize arguments
+        var_list =  self.normalize_typed_variable_list(p[3])
+        p[0] = ('atomic_formula_skeleton', p[2], var_list)
+        if self.debug:
+            print("Defined predicate:", p[2])
+            print("\t and variables:")
+            for entry in var_list:
+                print("var: {} term: {}".format(entry['var'], entry['term']))
+                print("var: {} term: {}".format(entry['var'], entry['term']))
+        # declare variables
+        for entry in var_list:
+            self.var_dict[entry['var']] = entry['term']
+
+    def p_predicate(self, p):
+        '''predicate    :   ID
+                        | rwAT'''
+        p[0] = p[1]
+
+    def p_list_of_typed_variables(self, p):
+        '''
+        list_of_typed_variables : VAR list_of_typed_variables
+                                | VAR MINUS type list_of_typed_variables
+                                | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        if len(p) == 3:
+            p[0] = [('var', p[1])] + p[2]
+            return
+        p[0] = [('var', p[1]), ('type', p[3])] + p[4]
+
+    def p_quantifier_scope(self, p):
+        '''
+        quantifier_scope : LPAREN VAR quantifier_scope
+                        | LPAREN VAR MINUS type quantifier_scope
+                        | VAR MINUS type quantifier_scope
+                        | VAR quantifier_scope
+                        | RPAREN'''
+        # production: quantifier_scope -> RPAREN
+        if p[1] == ')':
+            p[0] = []
+            return
+
+        if p[1] == '(':
+            # all variables captured
+            if len(p) == 4:
+                var_token_list = [('var', p[2])] + p[3]
+            else:
+                var_token_list = [('var', p[2]), ('type', p[4])] + p[5]
+
+            var_list = self.normalize_typed_variable_list(var_token_list)
+            # register all variables into scope
+            for entry in var_list:
+                self.var_dict[entry['var']] = entry['term']
+            p[0] = var_list
+            return
+
+        # production: quantifier_scope -> VAR quantifier_scope
+        if len(p) == 3:
+            p[0] = [('var', p[1])] + p[2]
+            return
+
+        # production: quantifier_scope -> VAR MINUS type quantifier_scope
+        p[0] = [('var', p[1]), ('type', p[3])] + p[4]
+
+
+    def p_functions_def(self, p):
+        '''
+        functions_def   : LPAREN rwFUNCTIONS function_typed_list RPAREN
+                        | LPAREN rwFUNCTIONS RPAREN'''
+        if len(p) == 4:
+            return
+        func_spec_list = p[3]
+        if self.debug:
+            print("Number of functions defined:", len(func_spec_list))
+        for func_spec in func_spec_list:
+            self.instance.process_function_skeleton(func_spec)
+        if self.debug:
+            for name, func_obj in self.instance.functions.items():
+                print("\t", func_obj)
+
+    def p_function_typed_list(self, p):
+        '''
+        function_typed_list : atomic_function_skeleton MINUS type function_typed_list
+                            | atomic_function_skeleton function_typed_list
+                            | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        if len(p) == 3:
+            p[0] = [p[1]] + p[2]
+            return
+        func_data = p[1]
+        func_data['codomain'] = p[3]
+        p[0] = [func_data] + p[4]
+
+    # def p_list_of_atomic_function_skeleton(self, p):
+    #     '''
+    #     list_of_atomic_function_skeleton    :  list_of_atomic_function_skeleton
+    #                                         | atomic_function_skeleton
+    #                                         | empty'''
+    #     pass
+
+    def p_atomic_function_skeleton(self, p):
+        '''
+        atomic_function_skeleton    : LPAREN function list_of_typed_variables RPAREN'''
+        p[0] = {
+            'type': 'atomic_function_skeleton',
+            'name': p[2],
+            'domain': self.normalize_typed_variable_list(p[3])
+        }
+
+    def p_function(self, p):
+        '''
+        function    : ID
+                    | rwAT'''
+        p[0] = p[1]
+
+    def p_domain_constraints_def(self, p):
+        '''domain_constraints_def   : LPAREN rwCONSTRAINTS con_GD RPAREN'''
+        pass
+
+    def p_con_GD(self, p):
+        '''
+        con_GD  : LPAREN rwAND con_GD_list RPAREN
+                | LPAREN rwFORALL LPAREN list_of_typed_variables RPAREN con_GD RPAREN
+                | LPAREN rwAT rwEND GD RPAREN
+                | LPAREN rwALWAYS GD RPAREN
+                | LPAREN rwSOMETIME GD RPAREN
+                | LPAREN rwWITHIN GD RPAREN
+                | LPAREN rwAT_MOST_ONCE GD RPAREN
+                | LPAREN rwSOMETIME_AFTER GD GD RPAREN
+                | LPAREN rwSOMETIME_BEFORE GD GD RPAREN
+                | LPAREN rwALWAYS_WITHIN NAT GD GD RPAREN
+                | LPAREN rwHOLD_DURING NAT NAT GD RPAREN
+                | LPAREN rwHOLD_AFTER NAT GD RPAREN'''
+        pass
+
+    def p_con_GD_list(self, p):
+        '''
+        con_GD_list : con_GD con_GD_list
+                    | empty'''
+        pass
+
+    def p_structure_def_list(self, p):
+        '''
+        structure_def_list  : structure_def structure_def_list
+                            | empty'''
+        pass
+
+    def p_structure_def(self, p):
+        '''
+        structure_def   : action_def
+                        | durative_action_def
+                        | derived_def'''
+        pass
+
+    def p_action_def(self, p):
+        '''
+        action_def  : LPAREN rwACTION action_symbol action_parameters action_def_body RPAREN'''
+        name = p[3]
+        parameters = p[4]
+        action_body = p[5]
+        if 'duration' in action_body and action_body['duration'] is None:
+            msg = "Error processing durative action: durative actions with unbounded duration are not supported"
+            raise SemanticError(self.lexer.lineno(), msg)
+        self.instance.process_action_skeleton(name, parameters, action_body)
+        # clear up scope
+        for entry in parameters:
+            del self.var_dict[entry['var']]
+
+    def p_action_parameters(self, p):
+        '''
+        action_parameters   : rwPARAMETERS LPAREN list_of_typed_variables RPAREN
+        '''
+        var_list = self.normalize_typed_variable_list(p[3])
+        for entry in var_list:
+            self.var_dict[entry['var']] = entry['term']
+        p[0] = var_list
+
+    def p_action_symbol(self, p):
+        '''
+        action_symbol   : ID'''
+        p[0] = p[1]
+
+    def p_action_def_body(self, p):
+        '''
+        action_def_body : rwPRECONDITION empty_or_pre_GD rwEFFECT empty_or_effect
+                        | rwEFFECT empty_or_effect'''
+        if len(p) == 5:
+            p[0] = {
+                'precondition': p[2],
+                'effect': p[4]
+            }
+            return
+        p[0] = {
+            'precondition': None,
+            'effect': p[2]
+        }
+
+    def p_empty_or_pre_GD(self, p):
+        '''
+        empty_or_pre_GD : pre_GD
+                        | LPAREN RPAREN'''
+        if len(p) == 3:
+            p[0] = None
+            return
+        p[0] = p[1]
+
+    def p_empty_or_effect(self, p):
+        '''
+        empty_or_effect : effect
+                        | LPAREN RPAREN'''
+        if len(p) == 3:
+            p[0] = None
+            return
+        p[0] = p[1]
+
+    # MRJ: preferences grammar is not compatible with LALR parsers...
+    # def p_list_of_pre_GD(self, p):
+    #     '''
+    #     list_of_pre_GD  : pre_GD list_of_pre_GD
+    #                     | empty'''
+    #     pass
+
+    # def p_pre_GD(self, p):
+    #     '''
+    #     pre_GD  : pref_GD
+    #             | LPAREN rwAND list_of_pre_GD RPAREN
+    #             | LPAREN rwFORALL LPAREN list_of_typed_variables RPAREN pre_GD RPAREN'''
+    #     pass
+
+    # def p_pref_GD(self, p):
+    #     '''
+    #     pref_GD : LPAREN rwPREFERENCE GD RPAREN
+    #             | LPAREN rwPREFERENCE pref_name GD RPAREN
+    #             | GD'''
+    #     pass
+
+    def p_pre_GD(self, p):
+        '''pre_GD   : GD'''
+        p[0] = p[1]
+
+    def p_pref_name(self, p):
+        '''pref_name   : ID'''
+        p[0] = p[1]
+
+    def p_list_of_GD(self, p):
+        '''
+        list_of_GD  : GD list_of_GD
+                    | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        p[0] = [p[1]] + p[2]
+
+    def p_GD(self, p):
+        '''
+        GD  : literal_of_term
+            | LPAREN rwAND list_of_GD RPAREN
+            | LPAREN rwOR list_of_GD RPAREN
+            | LPAREN rwNOT GD RPAREN
+            | LPAREN rwIMPLY GD GD RPAREN
+            | LPAREN rwEXISTS quantifier_scope GD RPAREN
+            | LPAREN rwFORALL quantifier_scope GD RPAREN
+            | f_comp'''
+        if len(p) == 2:
+            # processes literal of atom or arithmetic atom
+            p[0] = p[1]
+            return
+        if p[2] == self.lexer.symbols.rwAND:
+            p[0] = land(*p[3])
+        elif p[2] == self.lexer.symbols.rwOR:
+            p[0] = lor(*p[3])
+        elif p[2] == self.lexer.symbols.rwNOT:
+            p[0] = neg(p[3])
+        elif p[2] == self.lexer.symbols.rwIMPLY:
+            p[0] = lor(neg(p[3]), p[4])
+        elif p[2] == self.lexer.symbols.rwEXISTS:
+            if self.debug:
+                print('existential quantifier, scope tokens: {} formula: {}'.format(p[3], p[4]))
+            vars = p[3]
+            phi = p[4]
+            p[0] = QuantifiedFormula(Quantifier.Exists, [entry['term'] for entry in vars], phi)
+            # clear vars from scope
+            for entry in vars:
+                del self.var_dict[entry['var']]
+        elif p[2] == self.lexer.symbols.rwFORALL:
+            vars = p[3]
+            phi = p[4]
+            p[0] = QuantifiedFormula(Quantifier.Forall,  [entry['term'] for entry in vars], phi)
+            # clear vars from scope
+            for entry in vars:
+                del self.var_dict[entry['var']]
+        else:
+            assert False
+
+
+    def p_literal_of_term(self, p):
+        '''
+        literal_of_term : atomic_formula_of_term
+                        | LPAREN rwNOT atomic_formula_of_term RPAREN'''
+        if len(p) == 2:
+            if p[1]['boolean']:
+                p[0] = p[1]['lhs'] == 1
+            else:
+                p[0] = p[1]['lhs'] == p[1]['rhs']
+        else:
+            if p[3]['boolean']:
+                p[0] = p[3]['lhs'] == 0
+            else:
+                p[0] = neg(p[3]['lhs'] == p[3]['rhs'])
+
+    def p_atomic_formula_of_term(self, p):
+        '''
+        atomic_formula_of_term  : LPAREN predicate list_of_term RPAREN
+                                | LPAREN EQUA term term RPAREN'''
+        if len(p) == 5:
+            lhs_func = self.instance.get(p[2])
+            sub_terms = p[3]
+            lhs = lhs_func(*sub_terms)
+            p[0] = {
+                'boolean': True,
+                'lhs': lhs,
+                'rhs': 1
+            }
+        else:
+            p[0] = {
+                'boolean': False,
+                'lhs': p[3],
+                'rhs': p[4]
+            }
+
+
+    def p_list_of_term(self, p):
+        '''
+        list_of_term    : term list_of_term
+                        | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        p[0] = [p[1]] + p[2]
+
+    def p_term(self, p):
+        '''
+        term    : ID
+                | VAR
+                | LPAREN function list_of_term RPAREN'''
+
+        if len(p) > 2:
+            try:
+                func_name = self.instance.get(p[2])
+            except tsk.LanguageError as e:
+                msg = "Error parsing term in formula, function '{}' is not declared".format(p[2])
+                raise SemanticError(self.lexer.lineno(), msg)
+            sub_terms = p[3]
+            p[0] = func_name(*sub_terms)
+            return
+        if self.lexer.is_identifier(p[1]):
+            try:
+                constant_ref = self.instance.get(p[1])
+                p[0] = constant_ref
+            except tsk.LanguageError as e:
+                msg = "Error parsing term in formula, constant '{}' is not declared".format(p[1])
+                raise SemanticError(self.lexer.lineno(), msg)
+        elif self.lexer.is_variable(p[1]):
+            try:
+                var_ref = self.var_dict[p[1]]
+                p[0] = var_ref
+            except KeyError as e:
+                msg = "Error parsing term in formula, variable '{}' is not declared in the current scope".format(p[1])
+                raise SemanticError(self.lexer.lineno(), msg)
+
+    def p_function_term(self, p):
+        '''function_term   : LPAREN function list_of_term RPAREN'''
+        try:
+            func_name = self.instance.get(p[2])
+            sub_terms = p[3]
+            p[0] = func_name(*sub_terms)
+        except tsk.LanguageError as e:
+            msg = "Error parsing function term, function '{}' is not declared".format(p[2])
+            raise SemanticError(self.lexer.lineno(), msg)
+
+    def p_list_of_expression(self, p):
+        '''
+        list_of_expression  : f_exp list_of_expression
+                            | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        p[0] = [p[1]] + p[2]
+
+    def p_f_exp(self, p):
+        '''
+        f_exp   : NAT
+                | REAL
+                | VAR
+                | LPAREN binary_op f_exp f_exp RPAREN
+                | LPAREN multi_op f_exp f_exp list_of_expression RPAREN
+                | LPAREN MINUS f_exp RPAREN
+                | f_head'''
+        if len(p) == 2:
+            if isinstance(p[1], int):
+                p[0] = self.instance.int_const(p[1])
+                return
+            elif isinstance(p[1], float):
+                p[0] = self.instance.real_const(p[1])
+                return
+            elif isinstance(p[1], CompoundTerm):
+                p[0] = p[1]
+                return
+            elif self.lexer.is_variable(p[1]):
+                # check if variable in scope
+                try:
+                    var_ref = self.var_dict[p[1]]
+                    p[0] = var_ref
+                    return
+                except KeyError as e:
+                    msg = "Error parsing expression, variable '{}' is not declared in the current scope".format(
+                        p[1])
+                    raise SemanticError(self.lexer.lineno(), msg)
+            elif self.lexer.is_identifier(p[1]):
+                # check if it is a constant
+                try:
+                    constant_ref = self.instance.get(p[1])
+                    p[0] = constant_ref
+                    return
+                except tsk.LanguageError as e:
+                    msg = "Error parsing expression, constant '{}' is not declared".format(p[1])
+                    raise SemanticError(self.lexer.lineno(), msg)
+            else:
+                # raise error
+                msg = "Error parsing expression: '{}' is not a constant or variable symbol"
+                raise SemanticError(self.lexer.lineno(), msg)
+            p[0] = p[1]
+            return
+        if len(p) == 5:
+            p[0] = -p[3]
+            return
+        if len(p) == 6:
+            if p[2] == '-':
+                p[0] = p[3] - p[4]
+            elif p[2] == '/':
+                p[0] = p[3] / p[4]
+            return
+        expr_list = [p[3], p[4]] + p[5]
+        sym_expr = expr_list[0]
+        for i in range(1, len(expr_list)):
+            if p[2] == '+':
+                sym_expr = sym_expr + expr_list[i]
+            elif p[2] == '*':
+                sym_expr = sym_expr * expr_list[i]
+        p[0] = sym_expr
+
+
+    def p_f_head(self, p):
+        '''
+        f_head  : LPAREN function list_of_term RPAREN
+                | ID'''
+        if len(p) == 2:
+            try:
+                func_name = self.instance.get(p[1])
+                p[0] = func_name()
+            except tsk.LanguageError as e:
+                msg = "Error parsing expression, function '{}' is not declared".format(p[1])
+                raise SemanticError(self.lexer.lineno(), msg)
+            return
+
+        try:
+            func_name = self.instance.get(p[2])
+            sub_terms = p[3]
+            p[0] = func_name(*sub_terms)
+        except tsk.LanguageError as e:
+            msg = "Error parsing expression, function '{}' is not declared".format(p[1])
+            raise SemanticError(self.lexer.lineno(), msg)
+
+    def p_binary_op(self, p):
+        '''
+        binary_op   : multi_op
+                    | MINUS
+                    | DIV'''
+        p[0] = p[1]
+
+    def p_multi_op(self, p):
+        '''
+        multi_op    : PLUS
+                    | TIMES'''
+        p[0] = p[1]
+
+    def p_f_comp(self, p):
+        '''f_comp  : LPAREN binary_comp f_exp f_exp RPAREN'''
+        if p[2] == '<':
+            p[0] = p[3] < p[4]
+        elif p[2] == '<=':
+            p[0] = p[3] <= p[4]
+        elif p[2] == '>':
+            p[0] = p[3] > p[4]
+        elif p[2] == '>=':
+            p[0] = p[3] >= p[4]
+        elif p[2] == '=':
+            #print('equality: =({}/{}, {}/{})'.format(p[3], type(p[3]), p[4], type(p[4])))
+            p[0] = p[3] == p[4]
+        else:
+            assert False
+
+    def p_binary_comp(self, p):
+        '''
+        binary_comp :  GT
+                    | LT
+                    | EQUA
+                    | GEQ
+                    | LEQ'''
+        p[0] = p[1]
+
+    def p_effect(self, p):
+        '''
+        effect  : LPAREN rwAND list_of_c_effect RPAREN
+                | c_effect
+        '''
+        if len(p) == 2:
+            p[0] = [p[1]]
+            return
+        p[0] = p[3]
+
+    def p_list_of_c_effect(self, p):
+        '''
+        list_of_c_effect    : c_effect list_of_c_effect
+                            | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        p[0] = [p[1]] + p[2]
+
+    def p_c_effect(self, p):
+        '''
+        c_effect    : LPAREN rwFORALL LPAREN list_of_typed_variables RPAREN effect RPAREN
+                    | LPAREN rwWHEN GD cond_effect RPAREN
+                    | p_effect'''
+        if len(p) == 2:
+            p[0] = p[1]
+            return
+        if p[2] == self.lexer.symbols.rwFORALL:
+            msg = "Error parsing effect: universally quantified effects are not supported at the moment"
+            raise UnsupportedFeature(self.lexer.lineno(), msg)
+        if p[2] == self.lexer.symbols.rwWHEN:
+            msg = "Error parsing effect: conditional effects are not supported at the moment"
+            raise UnsupportedFeature(self.lexer.lineno(), msg)
+
+    def p_p_effect(self, p):
+        '''
+        p_effect    : LPAREN rwNOT atomic_formula_of_term RPAREN
+                    | atomic_formula_of_term
+                    | LPAREN assign_op f_head f_exp RPAREN
+                    | LPAREN rwASSIGN function_term term RPAREN
+                    | LPAREN rwASSIGN function_term rwUNDEFINED RPAREN'''
+        if len(p) == 2:
+            atomic_formula = p[1]
+            p[0] = AssignmentEffectData(lhs=atomic_formula['lhs'], rhs=atomic_formula['rhs'])
+            return
+        if p[2] == self.lexer.symbols.rwNOT:
+            atomic_formula = p[3]
+            if atomic_formula['boolean'] == False:
+                msg = "Error parsing action effect: negated literals over non-Boolean functions are not supported"
+                raise SemanticError(self.lexer.lineno(), msg)
+            p[0] = AssignmentEffectData(lhs=atomic_formula['lhs'], rhs=0)
+            return
+        if p[2] == self.lexer.symbols.rwASSIGN:
+            if isinstance(p[4], Term):
+                p[0] = AssignmentEffectData(lhs=p[3], rhs=p[4])
+                return
+            if p[4] == self.lexer.symbols.rwUNDEFINED:
+                msg = "Error parsing action effect: 'undefined' special constant is not supported at the moment"
+                raise UnsupportedFeature(self.lexer.lineno(), msg)
+            return
+        if p[2] == self.lexer.symbols.rwSCALE_UP:
+            lhs = p[3]
+            rhs = p[4]
+            p[0] = AssignmentEffectData(lhs=lhs, rhs=lhs * rhs)
+            return
+        elif p[2] == self.lexer.symbols.rwSCALE_DOWN:
+            lhs = p[3]
+            rhs = p[4]
+            p[0] = AssignmentEffectData(lhs=lhs, rhs=lhs * rhs)
+            return
+        elif p[2] == self.lexer.symbols.rwINCREASE:
+            lhs = p[3]
+            rhs = p[4]
+            p[0] = AssignmentEffectData(lhs=lhs, rhs=lhs + rhs)
+            return
+        elif p[2] == self.lexer.symbols.rwDECREASE:
+            lhs = p[3]
+            rhs = p[4]
+            p[0] = AssignmentEffectData(lhs=lhs, rhs=lhs - rhs)
+            return
+        msg = "Error parsing action effect: special assignment operator {} is not supported at the moment".format(p[2])
+        raise UnsupportedFeature(self.lexer.lineno(), msg)
+
+    def p_cond_effect(self, p):
+        '''
+        cond_effect : LPAREN rwAND list_of_p_effect RPAREN
+                    | p_effect'''
+        if len(p) == 2:
+            p[0] = [p[1]]
+            return
+        p[0] = p[3]
+
+    def p_list_of_p_effect(self, p):
+        '''
+        list_of_p_effect    : p_effect list_of_p_effect
+                            | empty'''
+        if p[1] is None:
+            p[0] = []
+        p[0] = [p[1]] + p[2]
+
+    def p_assign_op(self, p):
+        '''
+        assign_op   : rwASSIGN
+                    | rwSCALE_UP
+                    | rwSCALE_DOWN
+                    | rwINCREASE
+                    | rwDECREASE'''
+        p[0] = p[1]
+
+    def p_durative_action_def(self, p):
+        '''durative_action_def  : LPAREN rwDURATIVE_ACTION da_symbol action_parameters da_def_body RPAREN'''
+        name = p[3]
+        parameters = p[4]
+        body_data = p[5]
+        self.instance.process_action_skeleton(name, parameters, body_data)
+        # clear up scope
+        for entry in parameters:
+            del self.var_dict[entry['var']]
+
+    def p_da_symbol(self, p):
+        '''da_symbol   : ID'''
+        p[0] = p[1]
+
+    def p_da_def_body(self, p):
+        '''da_def_body : rwDURATION duration_constraint rwCONDITION empty_or_da_GD rwEFFECT empty_or_da_effect'''
+        p[0] = {
+            'duration': p[2],
+            'precondition': p[4],
+            'effect': p[6]
+        }
+
+    def p_empty_or_da_GD(self, p):
+        '''
+        empty_or_da_GD  : da_GD
+                        | LPAREN RPAREN'''
+        if len(p) == 3:
+            p[0] = None
+            return
+        if not isinstance(p[1], list):
+            # MRJ: we do this in order to capture singleton conditions
+            p[0] = [p[1]]
+        else:
+            p[0] = p[1]
+
+    def p_empty_or_da_effect(self, p):
+        '''
+        empty_or_da_effect  : da_effect
+                            | LPAREN RPAREN'''
+        if len(p) == 3:
+            p[0] = None
+            return
+        if not isinstance(p[1], list):
+            # MRJ: we do this in order to capture singleton effects
+            p[0] = [p[1]]
+        else:
+            p[0] = p[1]
+
+    def p_list_of_da_GD(self, p):
+        '''
+        list_of_da_GD   : da_GD list_of_da_GD
+                        | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        p[0] = [p[1]] + p[2]
+
+    def p_da_GD(self, p):
+        '''
+        da_GD   : pref_timed_GD
+                | LPAREN rwAND list_of_da_GD RPAREN
+                | LPAREN rwFORALL LPAREN list_of_typed_variables RPAREN da_GD RPAREN'''
+        if len(p) == 2:
+            p[0] = p[1]
+            return
+        if p[2] == self.lexer.symbols.rwAND:
+            # MRJ: Note that we do not attempt to convert the preconditions of durative actions into a
+            # conjunctive formula just yet
+            p[0] = p[3]
+            return
+        msg = "Error parsing precondition of durative action: universally quantified preconditions are not supported at the moment"
+        raise UnsupportedFeature(self.lexer.lineno(), msg)
+
+    def p_pref_timed_GD(self, p):
+        '''
+        pref_timed_GD   : timed_GD
+                        | LPAREN rwPREFERENCE timed_GD RPAREN
+                        | LPAREN rwPREFERENCE pref_name timed_GD RPAREN'''
+        if len(p) > 2:
+            assert p[2] == self.lexer.symbols.rwPREFERENCES
+            msg = "Error parsing precondition of durative action: preferences are not supported"
+            raise UnsupportedFeature(self.lexer.lineno(), msg)
+        p[0] = p[1]
+
+    def p_timed_GD(self, p):
+        '''
+        timed_GD    : LPAREN rwAT time_specifier GD RPAREN
+                    | LPAREN rwOVER interval GD RPAREN'''
+        if p[2] == self.lexer.symbols.rwAT:
+            p[0] = {
+                'type': 'instant',
+                'offset': p[3],
+                'formula': p[4]
+            }
+            return
+        p[0] = {
+            'type': 'interval',
+            'offset': None,
+            'formula': p[4]
+        }
+
+    def p_time_specifier(self, p):
+        '''
+        time_specifier  : rwSTART
+                        | rwEND'''
+        p[0] = p[1]
+
+    def p_interval(self, p):
+        '''interval    : rwALL'''
+        p[0] = p[1]
+
+    def p_duration_constraint(self, p):
+        '''
+        duration_constraint : LPAREN rwAND list_of_simple_duration_constraint RPAREN
+                            | LPAREN RPAREN
+                            | simple_duration_constraint'''
+        if len(p) == 2:
+            p[0] = p[1]
+            return
+        if len(p) == 3:
+            return
+        msg = "Error parsing durative action: complex duration constraints are not supported"
+        raise UnsupportedFeature(self.lexer.lineno(), msg)
+
+    def p_list_of_simple_duration_constraint(self, p):
+        '''
+        list_of_simple_duration_constraint  : simple_duration_constraint list_of_simple_duration_constraint
+                                            | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        p[0] = [p[1]] + p[2]
+
+    def p_simple_duration_constraint(self, p):
+        '''
+        simple_duration_constraint  : LPAREN d_op VAR d_value RPAREN
+                                    | LPAREN rwAT time_specifier simple_duration_constraint RPAREN'''
+        # Note that the VAR in the first rule needs to be ?duration
+        if len(p) > 6:
+            msg = "Error parsing duration of durative action: instant-specific duration constraints are not supported"
+            raise UnsupportedFeature(self.lexer.lineno(), msg)
+        if p[2] != '=':
+            msg = "Error parsing duration of durative action: inequality expressions in duration constraints are not supported"
+            raise UnsupportedFeature(self.lexer.lineno(), msg)
+        variable = p[3]
+        if variable != '?duration':
+            msg = "Error parsing duration of durative action: found variable '{}' rather than '?duration'".format(variable)
+            raise ParseError(self.lexer.lineno(), msg)
+        p[0] = p[4]
+
+    def p_d_op(self, p):
+        '''
+        d_op    : LEQ
+                | GEQ
+                | EQUA'''
+        p[0] = p[1]
+
+    def p_d_value(self, p):
+        '''
+        d_value : f_exp'''
+        p[0] = p[1]
+
+    def p_da_effect(self, p):
+        '''
+        da_effect   : LPAREN rwAND list_of_da_effect RPAREN
+                    | timed_effect
+                    | LPAREN rwFORALL LPAREN list_of_typed_variables RPAREN da_effect RPAREN
+                    | LPAREN rwWHEN da_GD timed_effect RPAREN'''
+        if len(p) == 2:
+            p[0] = p[1]
+            return
+        if p[2] == self.lexer.symbols.rwAND:
+            p[0] = p[3]
+            return
+        if p[2] == self.lexer.symbols.rwFORALL:
+            msg = "Error parsing effect of durative action: universally quantified effects are not supported at the moment"
+            raise UnsupportedFeature(self.lexer.lineno(), msg)
+        if p[2] == self.lexer.symbols.rwWHEN:
+            msg = "Error parsing effect of durative action: conditional effects are not supported at the moment"
+            raise UnsupportedFeature(self.lexer.lineno(), msg)
+
+    def p_list_of_da_effect(self, p):
+        '''
+        list_of_da_effect   : da_effect list_of_da_effect
+                            | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        p[0] = [p[1]] + p[2]
+
+    def p_timed_effect(self, p):
+        '''
+        timed_effect    : LPAREN rwAT time_specifier cond_effect RPAREN
+                        | LPAREN rwAT time_specifier f_assign_da RPAREN
+                        | LPAREN assign_op_t f_head f_exp_t RPAREN'''
+        if p[2] == self.lexer.symbols.rwAT:
+            p[0] = {
+                'type': 'timed_effect',
+                'instant': p[3],
+                'effect': p[4]
+            }
+            return
+        msg = "Error parsing effects of durative actions: extended assignment operators are not supported"
+        raise UnsupportedFeature(self.lexer.lineno(), msg)
+
+    def p_f_assign_da(self, p):
+        '''f_assign_da : LPAREN assign_op f_head f_exp_da RPAREN'''
+        if p[2] != self.lexer.symbols.rwASSIGN:
+            msg = "Error parsing effects of durative actions: assignment operators other than 'assign' are not supported at the moment"
+            raise UnsupportedFeature(self.lexer.lineno(), msg)
+        p[0] = AssignmentEffectData(lhs=p[3], rhs=p[4])
+
+    def p_f_exp_da(self, p):
+        '''
+        f_exp_da    : LPAREN assign_op  f_head f_exp_da RPAREN
+                    | LPAREN binary_op f_exp_da f_exp_da RPAREN
+                    | LPAREN multi_op f_exp_da list_of_f_exp_da RPAREN
+                    | LPAREN MINUS f_exp_da RPAREN
+                    | VAR
+                    | f_exp'''
+        # NOTE: the VAR above must be ?duration
+        msg = "Error parsing effects of durative actions: arithmetic expressions are not supported at the moment"
+        raise SemanticError(self.lexer.lineno(), msg)
+
+    def p_list_of_exp_da(self, p):
+        '''
+        list_of_f_exp_da    : f_exp_da list_of_f_exp_da
+                            | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        p[0] = [p[1]] + p[2]
+
+    def p_assign_op_t(self, p):
+        '''
+        assign_op_t : rwINCREASE
+                    | rwDECREASE'''
+        p[0] = p[1]
+
+    def p_f_exp_t(self, p):
+        '''
+        f_exp_t : LPAREN TIMES f_exp rwDT RPAREN
+                | LPAREN TIMES rwDT f_exp RPAREN
+                | rwDT'''
+        msg = "Error parsing effect of action: continuous time effects are not supported at the moment"
+        raise UnsupportedFeature(self.lexer.lineno(), msg)
+
+    def p_derived_def(self, p):
+        '''derived_def : LPAREN rwDERIVED atomic_formula_skeleton_vars GD RPAREN'''
+        description, symbol, var_list = p[3]
+        if self.debug:
+            print("Derived predicate head:", symbol)
+        # Check that the predicate is defined and there's no variable renaming
+        try:
+            head_pred = self.instance.predicates.get(symbol)
+        except ValueError as e:
+            msg = "Error parsing derived predicate, head symbol '{}' is not declared".format(symbol)
+            raise SemanticError(self.lexer.lineno(), msg)
+        for k, arg in enumerate(head_pred.domain):
+            if self.debug:
+                print('signature: {} provided: {}'.format(head_pred.domain[k], var_list[k]['type']))
+            if not head_pred.domain[k] == var_list[k]['type']:
+                msg = "Error parsing derived predicate, head predicate '{}' type mismatch, check definition in (:predicates ...)".format(symbol)
+                raise SemanticError(self.lexer.lineno(), msg)
+        dpred_body = p[4]
+        if self.debug:
+            print("Body: {} type: {}".format(dpred_body, type(dpred_body)))
+        self.instance.process_derived_predicate_skeleton(head_pred, var_list, dpred_body)
+        # clear up scope
+        for entry in var_list:
+            del self.var_dict[entry['var']]
+
+    def p_problem_require_def(self, p):
+        '''problem_require_def  : LPAREN rwREQUIREMENTS requirement_key_list RPAREN'''
+        self.check_requirements_support()
+
+    def p_object_declaration(self, p):
+        '''object_declaration   : LPAREN rwOBJECTS typed_list_of_names RPAREN'''
+        if p[1] is None:
+            if self.debug:
+                print("No objects defined")
+            return
+        def_list = p[3]
+
+        for entry in def_list:
+            if isinstance(entry, tuple):
+                typename, constant_list = entry
+                if typename not in self.instance.types:
+                    msg = "Error parsing (:objects ) section: type '{}' was not defined".format(typename)
+                    raise SemanticError(self.lexer.lineno(), msg)
+                self.instance.process_constant_definition(entry)
+            else:
+                msg = "Error processing (:objects ) section: constant '{}' has no type attached".format(entry)
+                raise SemanticError(self.lexer.lineno(), msg)
+        if self.debug:
+            total_constants = 0
+            for typename, constant_list in self.instance.domains.items():
+                total_constants += len(constant_list)
+            print("Total constants defined:", total_constants)
+
+    def p_init(self, p):
+        '''init    : LPAREN rwINIT list_of_init_el RPAREN'''
+        self.instance.init = p[3]
+
+    def p_list_of_init_el(self, p):
+        '''
+        list_of_init_el : init_el list_of_init_el
+                        | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        p[0] = [p[1]] + p[2]
+
+    def p_init_el(self, p):
+        '''
+        init_el : literal_of_name
+                | LPAREN rwAT REAL literal_of_name RPAREN
+                | LPAREN EQUA term REAL RPAREN
+                | LPAREN EQUA term NAT RPAREN
+                | LPAREN EQUA term ID RPAREN'''
+        # Need to check that terms have no variables!
+        if len(p) == 2:
+            p[0] = (p[1]['lhs'], p[1]['rhs'])
+            return
+        if p[2] == self.lexer.symbols.rwAT:
+            msg = "Error parsing initial state: temporal literals are not supported at the moment"
+            raise UnsupportedFeature(self.lexer.lineno(), msg)
+        if isinstance(p[4], float):
+            p[0] = (p[3], p[4])
+        elif isinstance(p[4], int):
+            p[0] = (p[3], p[4])
+        else:
+            try:
+                constant_term = self.instance.get(p[4])
+                p[0] = (p[3], constant_term)
+            except tsk.LanguageError as e:
+                msg = "Error processing initial state: object '{}' was not defined".format(p[4])
+                raise SemanticError(self.lexer.lineno(), msg)
+
+    def p_literal_of_name(self, p):
+        '''
+        literal_of_name : atomic_formula_of_name
+                        | LPAREN rwNOT atomic_formula_of_name RPAREN'''
+        if len(p) == 2:
+            p[0] = p[1]
+            return
+        else:
+            if p[3]['boolean']:
+                p[3]['rhs'] = 1 - p[3]['rhs']
+            else:
+                msg = "Error parsing literal of ground atom: Negation of atoms of non-boolean functions is not supported"
+                raise SemanticError(self.lexer.lineno(), msg)
+
+    def p_atomic_formula_of_name(self, p):
+        '''
+        atomic_formula_of_name  : LPAREN predicate list_of_term RPAREN
+                                | LPAREN EQUA term term RPAREN'''
+        if len(p) == 5:
+            try:
+                func_symbol = self.instance.get(p[2])
+            except tsk.LanguageError as e:
+                msg = "Error parsing ground atomic formula: function '{}' is not defined".format(p[2])
+                raise SemanticError(self.lexer.lineno(), msg)
+            sub_terms = p[3]
+            p[0] = {
+                'boolean': True,
+                'lhs': func_symbol(*sub_terms),
+                'rhs': 1
+            }
+            return
+        p[0] = {
+            'boolean': False,
+            'lhs': p[3],
+            'rhs': p[4]
+        }
+
+    def p_list_of_name(self, p):
+        '''
+        list_of_name    : ID list_of_name
+                        | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        p[0] = [p[1]] + p[2]
+
+    def p_goal(self, p):
+        '''goal    : LPAREN rwGOAL pre_GD RPAREN'''
+        self.instance.goal = p[3]
+
+    def p_problem_constraints_def(self, p):
+        '''problem_constraints_def  : LPAREN rwCONSTRAINTS con_GD RPAREN'''
+        pass
+
+    def p_metric_spec(self, p):
+        '''metric_spec  : LPAREN rwMETRIC optimization metric_f_exp RPAREN'''
+        self.instance.process_objective_definition(dict(mode=p[3], definition=p[4]))
+
+    def p_optimization(self, p):
+        '''
+        optimization    : rwMINIMIZE
+                        | rwMAXIMIZE'''
+        if p[1] == self.lexer.symbols.rwMINIMIZE:
+            p[0] = ObjectiveMode.MINIMIZE
+            return
+        p[0] = ObjectiveMode.MAXIMIZE
+
+    def p_metric_f_exp(self, p):
+        '''
+        metric_f_exp    : LPAREN binary_op metric_f_exp metric_f_exp RPAREN
+                        | LPAREN multi_op metric_f_exp list_of_metric_f_exp RPAREN
+                        | LPAREN MINUS metric_f_exp RPAREN
+                        | REAL
+                        | LPAREN ID list_of_name RPAREN
+                        | ID
+                        | LPAREN rwTOTAL_TIME RPAREN
+                        | LPAREN rwTOTAL_COST RPAREN
+                        | LPAREN rwIS_VIOLATED pref_name RPAREN'''
+        if p[2] == self.lexer.symbols.rwTOTAL_TIME:
+            p[0] = dict(type=ObjectiveType.TOTAL_TIME, expr=None)
+            return
+        if p[2] == self.lexer.symbols.rwTOTAL_COST:
+            p[0] = dict(type=ObjectiveType.TOTAL_COST, expr=None)
+            return
+        print("Warning: implementation of `metric` section still work in progress, objective is ignored")
+        p[0] = dict(type=ObjectiveType.UNSPECIFIED, expr=None)
+
+    def p_list_of_metric_f_exp(self, p):
+        '''
+        list_of_metric_f_exp    : metric_f_exp list_of_metric_f_exp
+                                | empty'''
+        if p[1] is None:
+            p[0] = []
+            return
+        p[0] = [p[1]] + p[2]
+
+    def p_empty(self, p):
+        '''empty    :'''
+        pass
+
+    def p_error(self, p):
+        if not p:
+            # reached End of File
+            return
+        if self.debug:
+            print('Syntax error in input! See log file: {}'.format(self.logfile))
+
+        print('Syntax error in input! Line: {} failed token: {} next: {}'.format(p.lineno, p, self._parser.token()))
+
+        while True:
+            tok = self._parser.token()
+            if not tok or tok.type == 'LPAREN':
+                break
+        self._parser.errok()
+
+        return tok
diff --git a/src/tarski/io/rddl.py b/src/tarski/io/rddl.py
index d8190b04..fa46cce9 100644
--- a/src/tarski/io/rddl.py
+++ b/src/tarski/io/rddl.py
@@ -219,7 +219,7 @@ def __init__(self, filename):
 
     @staticmethod
     def _load_rddl_model(filename):
-        with open(filename, 'r') as input_file:
+        with open(filename, 'r', encoding='utf8') as input_file:
             rddl = input_file.read()
         parser = modules.import_pyrddl_parser()()
         parser.build()
@@ -436,7 +436,7 @@ def write_model(self, filename):
             horizon=self.get_horizon(),
             discount=self.get_discount()
         )
-        with open(filename, 'w') as file:
+        with open(filename, 'w', encoding='utf8') as file:
             file.write(content)
 
     def get_requirements(self):
diff --git a/src/tarski/io/sas/__init__.py b/src/tarski/io/sas/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/tarski/io/sas/fd.py b/src/tarski/io/sas/fd.py
new file mode 100644
index 00000000..35328dd9
--- /dev/null
+++ b/src/tarski/io/sas/fd.py
@@ -0,0 +1,224 @@
+# tarski: An AI Planning Modeling Framework
+#
+# Author: Miquel Ramirez <miquel.ramirez@pm.me>
+# ----------------------------------------------------------------------------------------------------------------------
+# src/tarski/io/sas/fd.py
+#
+# SAS instance writer for Fast Downward pre-processor
+# ----------------------------------------------------------------------------------------------------------------------
+from tarski.io.sas.templates import variable_section_elem_tmpl, axiom_defaults_section_tmpl, precondition_elem_tmpl, \
+    effect_elem_tmpl, operator_tmpl, initial_value_list_elem_tmpl, initial_state_section_tmpl, \
+    goal_value_list_elem_tmpl, goal_state_section_tmpl, sas_instance_tmpl
+from tarski.syntax import symref, CompoundTerm
+
+
+FAST_DOWNWARD_SAS_VERSION = 4
+
+
+class Writer(object):
+
+    def __init__(self, **kwargs):
+        self.dom_theory = kwargs['theory']
+        self.state_variables = kwargs['state_variables']
+        self.domains = kwargs['domains']
+        self.actions = kwargs['actions']
+        self.initial = kwargs['initial']
+        self.goal = kwargs['goal']
+        self.unit_costs = kwargs.get('unit_costs', True)
+
+    def get_sas_version(self) -> str:
+        return '{}'.format(FAST_DOWNWARD_SAS_VERSION)
+
+    def get_metric_type(self) -> str:
+        return '{}'.format(self.unit_costs and 0 or 1)
+
+    def get_number_variables(self) -> str:
+        return '{}'.format(len(self.state_variables))
+
+    def make_value_section(self, var: CompoundTerm) -> str:
+        """
+        Makes values section in variable definition section
+        :param var:
+        :return:
+        """
+        value_list = []
+        for obj in self.domains[symref(var)].objects:
+            value_list += ['{}'.format(str(obj.expr))]
+        return '\n'.join(value_list)
+
+    def make_variable_section(self) -> str:
+        elements = []
+        for var in self.state_variables:
+            elements += [variable_section_elem_tmpl.substitute(
+                name=str(var.expr),
+                # @TODO: come up with proper value when we get axioms in fully
+                axiom_layer=-1,
+                domain_size=len(self.domains[var]),
+                value_section=self.make_value_section(var.expr)
+            )]
+
+        return '\n'.join(elements)
+
+    def get_number_mutex_groups(self) -> str:
+        """
+        Returns the number of mutex groups in the instance
+        :return:
+        """
+        # @TODO: review this when we come up with an idea of what are these constraints exactly
+        return '{}'.format(0)
+
+    def make_mutex_group_section(self) -> str:
+        """
+        Constructs the mutex group section of the SAS instance file
+        :return:
+        """
+        mutex_groups = []
+
+        # @TODO: implement when we have an idea of what are these constraints exactly
+        if len(mutex_groups) == 0:
+            return ''
+
+        return "\n".join(mutex_groups)
+
+    def make_axioms_defaults_list(self) -> str:
+        defaults = []
+
+        # @TODO: finish implementation when we have axioms in
+        if len(defaults) == 0:
+            return ''
+        return '\n' + '\n'.join(defaults)
+
+    def make_axiom_defaults_section(self) -> str:
+        # @TODO: finish implementation when we have axioms in
+        return axiom_defaults_section_tmpl.substitute(
+            num_axiom_vars=0,
+            axiom_defaults_list=self.make_axioms_defaults_list()
+        )
+
+    def get_number_axiom_rules(self) -> str:
+        # @TODO: finish implementation when we have axioms fully in
+        return '{}'.format(0)
+
+    def make_axiom_rules_section(self) -> str:
+        rules = []
+
+        # @TODO: finish implementation when we have it
+        if len(rules) == 0:
+            return ''
+        return '\n' + '\n'.join(rules)
+
+    def get_num_operators(self):
+        return '{}'.format(len(self.actions))
+
+    def get_action_name(self, a):
+        return "{}({})".format(a.name, ",".join([str(arg) for arg in a.arguments]))
+
+    def get_num_preconditions(self, a):
+        return len(a.transitions)
+
+    def make_precondition_list(self, a):
+        elems = []
+        for x, v, _ in a.transitions:
+            elems += [precondition_elem_tmpl.substitute(
+                variable_index=self.state_variables.get_index(symref(x)),
+                value_index=self.domains[symref(x)].get_index(symref(v))
+            )]
+        return '\n'.join(elems)
+
+    def get_num_effects(self, a):
+        return len(a.transitions)
+
+    def make_effect_list(self, a):
+        elems = []
+
+        for x, _, w in a.transitions:
+            elems += [effect_elem_tmpl.substitute(
+                num_effect_conditions=0,
+                effect_condition_list='',
+                affected_variable_index=self.state_variables.get_index(symref(x)),
+                new_value_index=self.domains[symref(x)].get_index(symref(w))
+            )]
+
+        return '\n'.join(elems)
+
+    def get_cost(self, a):
+        # @TODO: finish implementation when we have action costs
+        return '{}'.format(1)
+
+    def make_operators_section(self):
+        """
+        Constructs the operators section
+        :return:
+        """
+        operators = []
+        assert len(self.actions) > 0
+        for action in self.actions:
+            operators += [operator_tmpl.substitute(
+                name=self.get_action_name(action),
+                num_preconditions=self.get_num_preconditions(action),
+                precondition_list=self.make_precondition_list(action),
+                num_effects=self.get_num_effects(action),
+                effect_list=self.make_effect_list(action),
+                cost=self.get_cost(action)
+            )]
+        print(operators)
+        return '\n'.join(operators)
+
+    def make_initial_value_list(self) -> str:
+        """
+
+        :return:
+        """
+        values = []
+        for x, v in self.initial:
+            values += [initial_value_list_elem_tmpl.substitute(
+                variable_index=self.state_variables.get_index(symref(x)),
+                value_index=self.domains[symref(x)].get_index(symref(v))
+            )]
+
+        return '\n'.join(values)
+
+    def make_initial_state_section(self) -> str:
+        return initial_state_section_tmpl.substitute(
+            num_variables=len(self.state_variables),
+            initial_value_list=self.make_initial_value_list()
+        )
+
+    def make_goal_value_list(self) -> str:
+        values = []
+        for x, v in self.goal:
+            values += [goal_value_list_elem_tmpl.substitute(
+                variable_index=self.state_variables.get_index(symref(x)),
+                value_index=self.domains[symref(x)].get_index(symref(v))
+            )]
+        return '\n'.join(values)
+
+    def make_goal_state_section(self) -> str:
+        return goal_state_section_tmpl.substitute(
+            goal_value_list=self.make_goal_value_list()
+        )
+
+    def dump(self, fp) -> None:
+        """
+        Dumps instance data in file-like object `fp`
+        :param fp:
+        :return:
+        """
+
+        data = sas_instance_tmpl.substitute(
+            format_version=self.get_sas_version(),
+            metric_type=self.get_metric_type(),
+            num_variables=self.get_number_variables(),
+            variable_section=self.make_variable_section(),
+            num_mutex_groups=self.get_number_mutex_groups(),
+            mutex_group_section=self.make_mutex_group_section(),
+            axiom_defaults_section=self.make_axiom_defaults_section(),
+            num_axiom_rules=self.get_number_axiom_rules(),
+            axiom_rules_section=self.make_axiom_rules_section(),
+            num_operators=self.get_num_operators(),
+            operators_section=self.make_operators_section(),
+            initial_state_section=self.make_initial_state_section(),
+            goal_state_section=self.make_goal_state_section()
+        )
+
+        fp.write(data)
diff --git a/src/tarski/io/sas/json.py b/src/tarski/io/sas/json.py
new file mode 100644
index 00000000..a900e83d
--- /dev/null
+++ b/src/tarski/io/sas/json.py
@@ -0,0 +1,147 @@
+# tarski: An AI Planning Modeling Framework
+#
+# Author: Miquel Ramirez <miquel.ramirez@pm.me>
+# ----------------------------------------------------------------------------------------------------------------------
+# tarski/io/sas/json.py
+#
+# Planner-agnostic SAS serialization into JSON streams
+# ----------------------------------------------------------------------------------------------------------------------
+import json
+
+from tarski.syntax import symref
+from tarski.util import SymbolIndex
+
+
+def dump(lang, actions, initial, goal, objects, domains, name, fp):
+    """
+    Serializes instance elements (domain theory, actions, initial state and goal condition) into a JSON
+    formatted stream.
+
+    :param lang: Domain theory
+    :param actions: List of ground actions
+    :param initial: Initial state, a list of equality literals, given as pairs of terms
+    (symbol of the form $x(\bar{a})$) and values v (constant symbols)
+    :param goal: Goal state, a list of equality literals, given as pairs of term (as above) and values v (as above)
+    :param name: instance name
+    :param fp:
+    :return:
+    """
+
+    # `SymbolIndex` is a container class that allows to define a set of objects, where each element is indexed
+    X = SymbolIndex()
+
+    # We identify the set of terms by inspecting 1) action transition constraints, 2) the definition of the
+    # initial state, and 3) the structure of the (conjunctive) goal formula.
+    for act in actions:
+        for x, _, _ in act.transitions:
+            if symref(x) in X:
+                continue
+            X.add(symref(x))
+
+    for x, _ in initial:
+        if symref(x) in X:
+            continue
+        X.add(symref(x))
+
+    for x, _ in goal:
+        if symref(x) in X:
+            continue
+        X.add(symref(x))
+
+    # We aggregate all "named" constants in a set (domain) D
+    D = SymbolIndex()
+    for obj in objects:
+        if obj.sort == lang.Integer:
+            continue  # do not encode integers as objects
+        if symref(obj) in D:
+            continue
+        D.add(symref(obj))
+
+    # Depending on the planner, it may be convenient to have definitions of "types" subsets of D, the naturals,
+    # integers or the reals even
+    types_data = [
+        {"name": "object",
+         "domain": [str(o) for o in objects]}
+    ]
+
+    # Here we setup an index that defines the variables of the SAS instance. For each variable we give its
+    # symbolic representation (a string of characters like "on(A)"), and a reference to its domain (set of
+    # objects). Note that a domain does not need to be "tight" and may include many values that never appear
+    # in the set of solutions of transition constraints.
+
+    vars_data = []
+
+    for x in X.objects:
+        if x.expr.symbol.codomain == lang.Object:
+            vars_data += [
+                {"name": str(x.expr), "type": "object"}
+            ]
+        elif x.expr.symbol.codomain == lang.Integer:
+            # we need the bounds
+            vars_data += [
+                {"name": str(x.expr), "type": "int", "domain": [domains[x][0], domains[x][-1]]}
+            ]
+
+    vars_data = [
+        {"name": str(x.expr),
+         "type": "object"} for x in X.objects
+    ]
+
+    # Now we provide the list of symbolic representations for each of the actions in the instance
+    action_names = [
+        "{}({})".format(a.name, ",".join([str(arg) for arg in a.arguments])) for a in actions
+    ]
+
+    # The transition function is factored as table constraints $Tr(a,x)$, and for each constraint
+    # - indexed by action and variable - we give the pairs of values v and w s.t. for any tuple
+    # (s,a,s') \in Tr, s \models v, s' \models w.
+    trans_data = []
+
+    for k, a in enumerate(actions):
+        for x, v, w in a.transitions:
+            if x.symbol.codomain == lang.Object:
+                trans_data += [
+                    {"a": k,
+                     "x": X.get_index(symref(x)),
+                     "v": D.get_index(symref(v)),
+                     "w": D.get_index(symref(w))}
+                ]
+            elif x.symbol.codomain == lang.Integer:
+                trans_data += [
+                    {"a": k,
+                     "x": X.get_index(symref(x)),
+                     "v": v.symbol,
+                     "w": w.symbol}
+                ]
+    # We provide the definitions of initial and goal states as lists of pairs of indices into the
+    # set of variables and values in the domain D.
+    init_data = []
+
+    for x, v in initial:
+        if isinstance(v, int):
+            init_data += [[X.get_index(symref(x)), v]]
+        else:
+            init_data += [[X.get_index(symref(x)), D.get_index(symref(v))]]
+
+    goal_data = []
+    for x, v in goal:
+        if isinstance(v, int):
+            goal_data += [[X.get_index(symref(x)), v]]
+        else:
+            goal_data += [[X.get_index(symref(x)), D.get_index(symref(v))]]
+
+    # Now we put all the instance data together and build the JSON document
+    doc = {
+        "metadata": {
+            "domain": lang.name,
+            "instance": name
+        },
+        "types": types_data,
+        "vars": vars_data,
+        "actions": action_names,
+        "trans": trans_data,
+        "init": init_data,
+        "goal": goal_data
+    }
+
+    json.dump(doc, fp, indent=4)
diff --git a/src/tarski/io/sas/templates.py b/src/tarski/io/sas/templates.py
new file mode 100644
index 00000000..c7f2b10a
--- /dev/null
+++ b/src/tarski/io/sas/templates.py
@@ -0,0 +1,103 @@
+# tarski: An AI Planning Modeling Framework
+#
+# Author: Miquel Ramirez <miquel.ramirez@pm.me>
+# ----------------------------------------------------------------------------------------------------------------------
+# src/tarski/io/sas/templates.py
+#
+# Templates for Fast Downward SAS instance structure
+#
+# Source: https://www.fast-downward.org/TranslatorOutputFormat4
+# ----------------------------------------------------------------------------------------------------------------------
+
+from string import Template
+
+value_section_elem_tmpl = Template("""\
+Atom ${value_symbol}""")
+
+value_section_null_tmpl = Template("""\
+<none of those>""")
+
+variable_section_elem_tmpl = Template("""\
+begin_variable
+${name}
+${axiom_layer}
+${domain_size}
+${value_section}
+end_variable""")
+
+mutex_group_list_element_tmpl = Template("""\
+${index_v} ${index_value}""")
+
+mutex_group_section_elem_tmpl = Template("""\
+begin_mutex_group
+${group_size}
+${mutex_group_list}
+end_mutex_group""")
+
+axiom_defaults_list_elem_tmpl = Template("""\
+${variable_index} ${default_value_index}""")
+
+axiom_defaults_section_tmpl = Template("""\
+begin_axiom_defaults
+${num_axiom_vars}${axiom_defaults_list}
+end_axiom_defaults""")
+
+axiom_rule_condition_tmpl = Template("""\
+${variable_index} ${value_index}""")
+
+axiom_rule_tmpl = Template("""\
+begin_rule
+${axiom_rule_condition}
+${axiom_head_index} ${axiom_head_value}
+end_rule""")
+
+precondition_elem_tmpl = Template("""\
+${variable_index} ${value_index}""")
+
+effect_elem_tmpl = Template("""\
+${num_effect_conditions} ${effect_condition_list} ${affected_variable_index} ${new_value_index}""")
+
+operator_tmpl = Template("""\
+begin_operator
+${name}
+${num_preconditions}
+${precondition_list}
+${num_effects}
+${effect_list}
+${cost}
+end_operator""")
+
+initial_value_list_elem_tmpl = Template("""\
+${variable_index} ${value_index}""")
+
+initial_state_section_tmpl = Template("""\
+begin_initial_state
+${num_variables}
+${initial_value_list}
+end_initial_state""")
+
+goal_value_list_elem_tmpl = Template("""\
+${variable_index} ${value_index}""")
+
+goal_state_section_tmpl = Template("""\
+begin_goal
+${goal_value_list}
+end_goal""")
+
+sas_instance_tmpl = Template("""\
+begin_version
+${format_version}
+end_version
+begin_metric
+${metric_type}
+end_metric
+${num_variables}
+${variable_section}
+${num_mutex_groups}${mutex_group_section}
+${axiom_defaults_section}
+${num_axiom_rules}${axiom_rules_section}
+${num_operators}
+${operators_section}
+${initial_state_section}
+${goal_state_section}
+""")
diff --git a/src/tarski/model.py b/src/tarski/model.py
index 724a9e80..f7c2b1df 100644
--- a/src/tarski/model.py
+++ b/src/tarski/model.py
@@ -2,7 +2,7 @@
 from typing import Union
 
 from . import errors as err
-from .syntax import Function, Constant, CompoundTerm, symref
+from .syntax import Function, Constant, CompoundTerm, symref, Variable
 from .syntax.predicate import Predicate
 
 
@@ -47,8 +47,8 @@ def __init__(self, language, evaluator=None):
         self.evaluator = evaluator
         self.language = language
         # self.vocabulary = language.vocabulary()
-        self.function_extensions = dict()
-        self.predicate_extensions = dict()
+        self.function_extensions = {}
+        self.predicate_extensions = {}
 
     def __eq__(self, other):
         # TODO Improve the performance of this
@@ -215,7 +215,7 @@ def create(lang, evaluator=None):
 
 class ExtensionalFunctionDefinition:
     def __init__(self):
-        self.data = dict()
+        self.data = {}
 
     def set(self, point, value):
         assert isinstance(point, tuple)
diff --git a/src/tarski/modules.py b/src/tarski/modules.py
index a5144576..251b9dc9 100644
--- a/src/tarski/modules.py
+++ b/src/tarski/modules.py
@@ -10,7 +10,7 @@
 def import_scipy_special():
     try:
         import scipy.special as sci  # pylint: disable=import-outside-toplevel
-    except ImportError as _:
+    except ImportError:
         raise ImportError('The scipy module does not seem available. '
                           'Please try installing Tarski with the "arithmetic" extra.') from None
     return sci
@@ -19,7 +19,7 @@ def import_scipy_special():
 def import_numpy():
     try:
         import numpy as np  # pylint: disable=import-outside-toplevel
-    except ImportError as _:
+    except ImportError:
         raise ImportError('The numpy module does not seem available. '
                           'Please try installing Tarski with the "arithmetic" extra.') from None
     return np
@@ -28,7 +28,7 @@ def import_numpy():
 def import_pyrddl_parser():
     try:
         from pyrddl.parser import RDDLParser  # pylint: disable=import-outside-toplevel
-    except ImportError as _:
+    except ImportError:
         raise ImportError('The pyrddl module does not seem available. '
                           'Please try installing Tarski with the "rddl" extra.') from None
     return RDDLParser
diff --git a/src/tarski/ndl/temporal.py b/src/tarski/ndl/temporal.py
index 031d1570..7a9a90ac 100644
--- a/src/tarski/ndl/temporal.py
+++ b/src/tarski/ndl/temporal.py
@@ -9,7 +9,6 @@
     2017
 """
 from ..syntax import Atom, CompoundTerm, CompoundFormula, Constant, symref, Connective, Tautology
-from ..model import Model
 
 
 class NDLSyntaxError(Exception):
@@ -36,6 +35,7 @@ def __init__(self, **kwargs):
     def __str__(self):
         return "LOCK {} AFTER {} FOR {}".format(self.r, self.ts, self.td)
 
+
 class ResourceLevel:
 
     def __init__(self, **kwargs):
@@ -56,19 +56,20 @@ def __str__(self):
         return "LOCK {} AFTER {} FOR {}".format(self.r, self.ts, self.td)
 
 
-class SetLiteralEffect(object):
+class SetLiteralEffect:
     """
     Set literal truth value
     """
 
     def __init__(self, lit, value):
-        self.l = lit
+        self.lit = lit
         self.value = value
 
     def __str__(self):
-        return "SET({}, {})".format(self.l, self.value)
+        return "SET({}, {})".format(self.lit, self.value)
+
 
-class AssignValueEffect(object):
+class AssignValueEffect:
     """
     Sets equality constraint
     """
@@ -80,7 +81,8 @@ def __init__(self, atom, value):
     def __str__(self):
         return "ASSIGN({}, {})".format(self.atom, self.value)
 
-class UniversalEffect(object):
+
+class UniversalEffect:
     """
     Forall effect
     """
@@ -92,7 +94,8 @@ def __init__(self, variable, effect):
     def __str__(self):
         return "FORALL({}, {})".format(self.var, self.effect)
 
-class ConditionalEffect(object):
+
+class ConditionalEffect:
     """
     If Then Else effect
     """
@@ -105,7 +108,8 @@ def __init__(self, cond, then_eff, else_eff):
     def __str__(self):
         return "IF ({}) \nTHEN {}\n ELSE {}".format(self.condition, self.then_eff, self.else_eff)
 
-class TimedEffect(object):
+
+class TimedEffect:
     """
     (t, eff) time-delayed effect
     """
@@ -118,7 +122,7 @@ def __str__(self):
         return "AFTER {} APPLY {}".format(self.delay, self.eff)
 
 
-class UnionExpression(object):
+class UnionExpression:
     """
     A union set expression
     """
@@ -128,10 +132,10 @@ def __init__(self, lhs, rhs):
         self.rhs = rhs
 
 
-def is_literal(l):
-    if not isinstance(l, Atom):
-        if isinstance(l, CompoundFormula):
-            return l.connective == Connective.Not and isinstance(l.subformulas[0], Atom)
+def is_literal(lit):
+    if not isinstance(lit, Atom):
+        if isinstance(lit, CompoundFormula):
+            return lit.connective == Connective.Not and isinstance(lit.subformulas[0], Atom)
         return False
     return True
 
@@ -155,15 +159,26 @@ def __init__(self, **kwargs):
         self.name = kwargs['name']
         self.parameters = kwargs['parameters']
         self.max_eff_time = 0.0
+        self.duration = kwargs.get('duration', None)
+        self.grounding_constraints = kwargs.get('grounding_constraints', [])
         self.effect_times = {}
         # precondition
         prec = kwargs['precondition']
         if not isinstance(prec, CompoundFormula) \
                 and not isinstance(prec, Atom)\
                 and not isinstance(prec, Tautology):
-            raise NDLSyntaxError(
-                "NDL Syntactic Error: precondition of action must be a compound formula, atom or tautology (given: {})".format(prec))
+            raise NDLSyntaxError("NDL Syntactic Error: precondition of action must be a compound formula,"
+                                 " atom or tautology (given: {})".format(prec))
         self.precondition = prec
+        # post-condition
+        post = kwargs.get('postcondition', None)
+        if post is not None:
+            if not isinstance(post, CompoundFormula) \
+                    and not isinstance(post, Atom)\
+                    and not isinstance(post, Tautology):
+                raise NDLSyntaxError("NDL Syntactic Error: post-condition of action must be a compound formula,"
+                                     " atom or tautology (given: {})".format(prec))
+        self.postcondition = post
         # resource requirements
         self.locks = []
         self.levels = []
@@ -188,15 +203,14 @@ def __init__(self, **kwargs):
             if isinstance(wrapped_effect, AssignValueEffect):
                 self.effect_times[symref(wrapped_effect.atom == eff.eff.value)] = eff.delay
             elif isinstance(wrapped_effect, SetLiteralEffect):
-                self.effect_times[(symref(wrapped_effect.l), wrapped_effect.value)] = eff.delay
+                self.effect_times[(symref(wrapped_effect.lit), wrapped_effect.value)] = eff.delay
             else:
                 raise NotImplementedError("Effects of type {} cannot be handled yet".format(type(wrapped_effect)))
-        for l in kwargs['untimed_effects']:
-            self.untimed_effects += [(0, l)]
-
-    def get_effect_time(self, l):
-        return self.effect_times[l]
+        for elem in kwargs['untimed_effects']:
+            self.untimed_effects += [(0, elem)]
 
+    def get_effect_time(self, elem):
+        return self.effect_times[elem]
 
 
 # class Instance:
diff --git a/src/tarski/reachability/__init__.py b/src/tarski/reachability/__init__.py
index fdde6c85..9b9907a8 100644
--- a/src/tarski/reachability/__init__.py
+++ b/src/tarski/reachability/__init__.py
@@ -1,3 +1,5 @@
 
 from .asp import create_reachability_lp
 from .clingo_wrapper import run_clingo, parse_model
+
+__all__ = ['create_reachability_lp', 'run_clingo', 'parse_model']
diff --git a/src/tarski/reachability/asp.py b/src/tarski/reachability/asp.py
index 421e65c4..03d1871c 100644
--- a/src/tarski/reachability/asp.py
+++ b/src/tarski/reachability/asp.py
@@ -347,8 +347,8 @@ def _ensure_list(elem):
 
 class Translator:
     def __init__(self):
-        self.d = dict()
-        self.inv = dict()
+        self.d = {}
+        self.inv = {}
 
     def normalize(self, name: str, prefix=''):
         """ Translate a given name and store the translation """
diff --git a/src/tarski/reachability/clingo_wrapper.py b/src/tarski/reachability/clingo_wrapper.py
index f300030b..8b6a23e7 100644
--- a/src/tarski/reachability/clingo_wrapper.py
+++ b/src/tarski/reachability/clingo_wrapper.py
@@ -1,36 +1,57 @@
 import logging
 import os
+import sys
 import shutil
 import tempfile
+from pathlib import Path
 from collections import defaultdict
 
-from ..errors import CommandNotFoundError, ExternalCommandError, OutOfMemoryError, OutOfTimeError
+from typing import List
+
+from ..errors import CommandNotFoundError, ExternalCommandError, OutOfMemoryError, OutOfTimeError, ArgumentError
 from ..utils import command as cmd
+from importlib.util import find_spec
 
 
-def run_clingo(lp):
+def get_gringo_command() -> List[str]:
+    """ Assemble the command to run gringo. First try to find a pip-installed gringo, then fall back to a system-wide
+    gringo installation. Raise exception if no gringo is found.
+
+    :return: A list with the command and arguments to invoke gringo.
+    """
+    if find_spec("clingo"):
+        logging.debug("Using the clingo pypi bindings to emulate gringo binary")
+        return [sys.executable, os.path.join(Path(__file__).parent.absolute(), "gringo.py")]
+
     gringo = shutil.which("gringo")
     if gringo is None:
         raise CommandNotFoundError("gringo")
 
+    logging.debug(f'Using gringo binary found in "{gringo}"')
+    return [gringo]
+
+
+def run_clingo(lp):
+    gringo_command = get_gringo_command()
+
     with tempfile.NamedTemporaryFile(mode='w+t', delete=False) as f:
         _ = [print(str(r), file=f) for r in lp.rules]
         _ = [print(str(r), file=f) for r in lp.directives]
         theory_filename = f.name
 
-    logging.debug('Using gringo binary found in "{}"'.format(gringo))
     errlog = ''
     with tempfile.NamedTemporaryFile(mode='w+t', delete=False) as f:
         with tempfile.NamedTemporaryFile(mode='w+t', delete=False) as stderr:
             # Option "-t" enforces an easier-to-parse textual output. Warnings could also be supressed with
             # option "-Wno-atom-undefined"
-            retcode = cmd.execute([gringo, "-t", theory_filename], stdout=f, stderr=stderr)
+            retcode = cmd.execute(gringo_command + ["--text", theory_filename], stdout=f, stderr=stderr)
             model_filename = f.name
+
             if retcode == 0:
                 return model_filename, theory_filename
 
     if os.path.isfile(stderr.name):
-        with open(stderr.name, 'r') as file:
+        with open(stderr.name, 'r', encoding='utf8') as file:
             errlog = file.read()
 
     if 'std::bad_alloc' in errlog:
@@ -41,21 +62,30 @@ def run_clingo(lp):
     raise ExternalCommandError(f"Unknown Gringo error. Gringo exited with code {retcode}. Full error log: {errlog}")
 
 
-def parse_model(filename, symbol_mapping):
+def parse_model(*, filename=None, content=None, symbol_mapping):
+    if filename and not content:
+        with open(filename, "r", encoding='utf8') as f:
+            return _parse_model(f, symbol_mapping)
+    elif content and not filename:
+        return _parse_model(content.splitlines(), symbol_mapping)
+    else:
+        raise ArgumentError("Cannot have both filename and content as arguments.")
+
+
+def _parse_model(lines, symbol_mapping):
     tr = symbol_mapping
     model = defaultdict(set)
-    with open(filename, "r") as f:
-        for line in f:
-            data = line.rstrip(' \n.').rstrip(')')
-            components = data.split('(')
-            if len(components) == 1:
-                symbol = tr.back(components[0])
-                model[symbol].add(())
-            elif len(components) == 2:
-                symbol, arguments = components
-                model[tr.back(symbol)].add(tuple(tr.back(s) for s in arguments.split(',')))
-            else:
-                # No nested terms expected, so there should be at most 2 components
-                raise RuntimeError('Unexpected line "{}" in Clingo solution file'.format(line))
+    for line in lines:
+        data = line.rstrip(' \n.').rstrip(')')
+        components = data.split('(')
+        if len(components) == 1:
+            symbol = tr.back(components[0])
+            model[symbol].add(())
+        elif len(components) == 2:
+            symbol, arguments = components
+            model[tr.back(symbol)].add(tuple(tr.back(s) for s in arguments.split(',')))
+        else:
+            # No nested terms expected, so there should be at most 2 components
+            raise RuntimeError(f'Unexpected line "{line}" in Clingo solution file')
 
     return model
diff --git a/src/tarski/reachability/gringo.py b/src/tarski/reachability/gringo.py
new file mode 100644
index 00000000..aad7ab3a
--- /dev/null
+++ b/src/tarski/reachability/gringo.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+import sys
+
+try:
+    from clingo.application import Application, clingo_main  # type: ignore
+except ModuleNotFoundError:
+    print("Clingo not installed")
+    sys.exit(-1)
+
+
+class WrapperClingo(Application):
+    """
+    A wrapper to emulate the default clingo app behavior
+    """
+    def __init__(self, name):
+        self.program_name = name
+
+    def main(self, ctl, files):
+        """
+        The default implementation from clingo documentation
+        Note- main(...) must be implemented
+        """
+        for f in files:
+            ctl.load(f)
+        if not files:
+            ctl.load("-")
+        ctl.ground([("base", [])])
+        ctl.solve()
+
+
+# run the clingo application in the default gringo mode
+clingo_main(WrapperClingo("gringo"), ["--mode", "gringo"] + sys.argv[1:])
diff --git a/src/tarski/sas/__init__.py b/src/tarski/sas/__init__.py
new file mode 100644
index 00000000..a31a4c77
--- /dev/null
+++ b/src/tarski/sas/__init__.py
@@ -0,0 +1,14 @@
+# tarski: An AI Planning Modeling Framework
+#
+# Author: Miquel Ramirez <miquel.ramirez@pm.me>
+# ----------------------------------------------------------------------------------------------------------------------
+# src/tarski/sas/__init__.py
+#
+# SAS Modeling support
+# ----------------------------------------------------------------------------------------------------------------------
+
+from collections import namedtuple
+
+
+Schema = namedtuple('Schema', ['name', 'variables', 'constraints', 'transitions'])
+Action = namedtuple('Action', ['name', 'arguments', 'transitions'])
diff --git a/src/tarski/sas/temporal.py b/src/tarski/sas/temporal.py
new file mode 100644
index 00000000..63e72df6
--- /dev/null
+++ b/src/tarski/sas/temporal.py
@@ -0,0 +1,49 @@
+"""
+    Temporal (durative) actions for SAS+
+"""
+from tarski.sas import Action
+
+
+class TemporalAction:
+    """
+    Temporal action where events are given as SAS+ actions
+    """
+    def __init__(self, **kwargs):
+        self.name = kwargs['name']
+        self.events = []
+        self.deltas = []
+
+        for delta, evt in kwargs.get('events', []):
+            self.events += [evt]
+            self.deltas += [delta]
+
+    @property
+    def start(self) -> Action:
+        """
+            First event in temporal action
+        """
+        return self.events[0]
+
+    @property
+    def end(self) -> Action:
+        """
+            Last event in temporal action
+        :return:
+        """
+        return self.events[-1]
+
+    @property
+    def duration(self):
+        """
+        Duration of the temporal action
+        :return:
+        """
+        return sum(self.deltas)
+
+    def add_event(self, d: float, evt: Action) -> None:
+        """
+        Adds new event to temporal action
+        :return:
+        """
+        self.deltas += [d]
+        self.events += [evt]
diff --git a/src/tarski/sas/util.py b/src/tarski/sas/util.py
new file mode 100644
index 00000000..4c663f31
--- /dev/null
+++ b/src/tarski/sas/util.py
@@ -0,0 +1,117 @@
+# tarski: An AI Planning Modeling Framework
+#
+# Author: Miquel Ramirez <miquel.ramirez@pm.me>
+# ----------------------------------------------------------------------------------------------------------------------
+# tarski/sas/utility.py
+#
+# Utility method to process SAS
+# ----------------------------------------------------------------------------------------------------------------------
+from itertools import product
+import tarski.model
+from tarski.evaluators.simple import evaluate
+from tarski.syntax import symref
+from tarski.theories import Theory
+from tarski.syntax.transform.substitutions import substitute_expression, create_substitution
+
+from tarski.sas import Action
+from tarski.sas.temporal import TemporalAction
+
+
+def check_constraints(C, s, subst):
+    r"""
+    Returns true if s \models C[x/subst(x)], that is, the result of replacing every variable symbol x by
+    subst(x) in C is satisfiable under s.
+    :param C: A constraint (predicate)
+    :param s: A semantic structure (model) object
+    :param subst: A mapping of variable symbols in C to constants
+    :return:
+    """
+    return all([s[substitute_expression(c, subst)] for c in C])
+
+
+def ground_action_schemas(lang, schemas, domains=None, sem_struct=None):
+    """
+    Straightforward grounding by enumeration
+    :param lang: domain theory
+    :param schemas: action schemas to be enumerated
+    :param domains: table with variable symbols domains, or None if domains provided inline
+    :param sem_struct: semantic structure to check the satisfiability of schema constraints, or None if all
+    predicates in constraints are built-ins (e.g. relational operators, equality)
+    :return:
+    """
+
+    actions = []
+    if sem_struct is None:
+        s = tarski.model.create(lang)
+        s.evaluator = evaluate
+    else:
+        s = sem_struct
+
+    for sch in schemas:
+        sch_x = [entry[0] for entry in sch.variables]
+        if domains is None:
+            sch_D = [entry[1] for entry in sch.variables]
+        else:
+            sch_D = [domains[entry[1]] for entry in sch.variables]
+
+        for a in product(*sch_D):
+            subst = create_substitution(sch_x, a)
+            if not check_constraints(sch.constraints, s, subst):
+                continue
+
+            action_a = Action(name=sch.name,
+                              arguments=a,
+                              transitions=[(substitute_expression(x, subst),
+                                            substitute_expression(pre, subst),
+                                            substitute_expression(post, subst)) for x, pre, post in sch.transitions])
+            actions += [action_a]
+            # print(action_a.name, a)
+
+    return actions
+
+
+def ground_temporal_action(lang, s, schema, domains=None):
+    """
+    Grounds temporal actions events
+    :param lang:
+    :param schema:
+    :return:
+    """
+    # 1. consolidate schema variables and constraints
+    X = set()
+    D = {}
+    for evt in schema.events:
+        for x_e, D_x_e in evt.variables:
+            if symref(x_e) in X:
+                continue
+            X.add(symref(x_e))
+            D[symref(x_e)] = D_x_e
+
+    sch_x = [x.expr for x in X]
+    sch_D = [D[symref(x_k)] for x_k in sch_x]
+
+    actions = []
+
+    for a in product(*sch_D):
+        subst = create_substitution(sch_x, a)
+        ground_events = []
+        for evt in schema.events:
+            if not check_constraints(evt.constraints, s, subst):
+                break
+            ground_events += [
+                Action(name=evt.name,
+                       arguments=a,
+                       transitions=[
+                           (substitute_expression(x, subst),
+                            substitute_expression(pre, subst),
+                            substitute_expression(post, subst)) for x, pre, post in evt.transitions
+                       ])
+            ]
+        if len(ground_events) != len(schema.events):
+            # one event grounding failed due to constraints
+            continue
+        actions += [TemporalAction(
+            name='{}({})'.format(schema.name, ','.join([str(c) for c in a])),
+            events=[(schema.deltas[k], gnd_evt) for k, gnd_evt in enumerate(ground_events)])]
+
+    return actions
diff --git a/src/tarski/syntax/formulas.py b/src/tarski/syntax/formulas.py
index 5bc9daec..df0d5f31 100644
--- a/src/tarski/syntax/formulas.py
+++ b/src/tarski/syntax/formulas.py
@@ -1,4 +1,3 @@
-
 from collections import OrderedDict
 from enum import Enum
 from typing import List
@@ -33,6 +32,7 @@ def negate_quantifier(q: Quantifier):
 
 class Formula:
     """ A first-order logical formula. """
+
     def __str__(self):
         raise NotImplementedError()  # To be subclassed
 
@@ -68,6 +68,7 @@ def is_syntactically_equal(self, other):
 class Tautology(Formula):
     def __str__(self):
         return "T"
+
     __repr__ = __str__
 
     def __eq__(self, other):
@@ -80,6 +81,7 @@ def __hash__(self):
 class Contradiction(Formula):
     def __str__(self):
         return "F"
+
     __repr__ = __str__
 
     def __eq__(self, other):
@@ -115,12 +117,13 @@ def __str__(self):
 
         inner = " {} ".format(self.connective).join(str(f) for f in self.subformulas)
         return "({})".format(inner)
+
     __repr__ = __str__
 
     def __eq__(self, other):
-        return self.__class__ is other.__class__ and \
-               self.connective == other.connective and \
-               self.subformulas == other.subformulas
+        return (self.__class__ is other.__class__
+                and self.connective == other.connective
+                and self.subformulas == other.subformulas)
 
     def __hash__(self):
         element_hashes = [self.__class__, self.connective]
@@ -146,13 +149,14 @@ def _check_well_formed(self):
     def __str__(self):
         vars_ = ', '.join(str(x) for x in self.variables)
         return '{} {} : ({})'.format(self.quantifier, vars_, self.formula)
+
     __repr__ = __str__
 
     def __eq__(self, other):
-        return self.__class__ is other.__class__ \
-               and self.quantifier == other.quantifier \
-               and termlists_are_equal(self.variables, other.variables) \
-               and self.formula == other.formula
+        return (self.__class__ is other.__class__
+                and self.quantifier == other.quantifier
+                and termlists_are_equal(self.variables, other.variables)
+                and self.formula == other.formula)
 
     def __hash__(self):
         return hash((self.__class__, self.quantifier, termlist_hash(self.variables), self.formula))
@@ -325,12 +329,13 @@ def _check_well_formed(self):
 
     def __str__(self):
         return '{}({})'.format(self.predicate.symbol, ','.join(str(t) for t in self.subterms))
+
     __repr__ = __str__
 
     def __eq__(self, other):
-        return self.__class__ is other.__class__ and \
-               self.predicate == other.predicate and \
-               termlists_are_equal(self.subterms, other.subterms)
+        return (self.__class__ is other.__class__
+                and self.predicate == other.predicate
+                and termlists_are_equal(self.subterms, other.subterms))
 
     def __hash__(self):
         return hash((self.__class__, self.predicate, termlist_hash(self.subterms)))
@@ -338,6 +343,7 @@ def __hash__(self):
 
 class VariableBinding:
     """ A VariableBinding contains a set of logical variables which are _bound_ in some formula or term """
+
     def __init__(self, variables=None):
         variables = variables or []
         # An (ordered) map between variable name and the variable itself:
@@ -387,4 +393,5 @@ def __iter__(self):
 
     def __str__(self):
         return f"Variables({','.join(map(str, self._v_values))})"
+
     __repr__ = __str__
diff --git a/src/tarski/syntax/ops.py b/src/tarski/syntax/ops.py
index f692ee9f..0aa3ee41 100644
--- a/src/tarski/syntax/ops.py
+++ b/src/tarski/syntax/ops.py
@@ -4,7 +4,7 @@
 from .. import modules
 from .sorts import children, compute_direct_sort_map, Interval
 from .visitors import CollectFreeVariables
-from .terms import Term, Constant, Variable
+from .terms import Term, Constant, Variable, CompoundTerm
 from .formulas import CompoundFormula, Connective
 from .symrefs import symref
 
@@ -21,7 +21,8 @@ def cast_to_closest_common_numeric_ancestor(lang, lhs, rhs):
     if isinstance(lhs, Term):
         if isinstance(rhs, np.ndarray):  # lhs is scalar, rhs is matrix
             return lhs.language.matrix([[lhs]], lhs.sort), rhs
-
+        if isinstance(rhs, (CompoundTerm, Variable)):
+            return lhs, rhs
         return lhs, Constant(lhs.sort.cast(rhs), lhs.sort)
 
     if isinstance(lhs, np.ndarray):  # lhs is matrix
diff --git a/src/tarski/syntax/sorts.py b/src/tarski/syntax/sorts.py
index a5a8e37d..28877bd9 100644
--- a/src/tarski/syntax/sorts.py
+++ b/src/tarski/syntax/sorts.py
@@ -158,7 +158,7 @@ def domain(self):
         if self.builtin or self.upper_bound - self.lower_bound > 9999:  # Yes, very hacky
             raise err.TarskiError(f'Cannot iterate over interval with range [{self.lower_bound}, {self.upper_bound}]')
         from . import Constant  # pylint: disable=import-outside-toplevel  # Avoiding circular references
-        return (Constant(x, self) for x in range(self.lower_bound, self.upper_bound+1))
+        return (Constant(x, self) for x in range(self.lower_bound, self.upper_bound + 1))
 
 
 def inclusion_closure(s: Sort) -> Generator[Sort, None, None]:
@@ -191,7 +191,7 @@ def children(s: Sort) -> Set[Sort]:
 
 def int_encode_fn(x):
     if isinstance(x, float) and not x.is_integer():
-        raise ValueError()  # We don't want 1.2 to get encoded as an int
+        raise ValueError(x)  # We don't want 1.2 to get encoded as an int
     return int(x)
 
 
diff --git a/src/tarski/syntax/terms.py b/src/tarski/syntax/terms.py
index a508e799..85d15b0c 100644
--- a/src/tarski/syntax/terms.py
+++ b/src/tarski/syntax/terms.py
@@ -1,4 +1,3 @@
-
 from typing import Tuple
 
 from .util import termlists_are_equal, termlist_hash
@@ -17,7 +16,7 @@ class Term:
         >>> from tarski.fstrips import language
         >>> from tarski.syntax import symref
         >>> lang = language("test")
-        >>> counter = dict()
+        >>> counter = {}
         >>> c = lang.constant('c', 'object')
         >>> counter[c] = 2  # ERROR: will not work correctly
         >>> counter[symref(c)] = 2  # This is the correct way of doing it
@@ -125,7 +124,7 @@ def is_syntactically_equal(self, other):
 
     # def __hash__(self):
     #     Raise an informative exception to prevent wrong usage of terms in associative containers
-        # raise err.WrongTermUsageError()
+    # raise err.WrongTermUsageError()
     # @see https://docs.python.org/3/reference/datamodel.html#object.__hash__
     __hash__ = None  # type: ignore
 
@@ -206,9 +205,9 @@ def hash(self):
         return hash((self.symbol.symbol, termlist_hash(self.subterms)))
 
     def is_syntactically_equal(self, other):
-        return self.__class__ is other.__class__ and \
-               self.symbol == other.symbol and \
-               termlists_are_equal(self.subterms, other.subterms)
+        return (self.__class__ is other.__class__
+                and self.symbol == other.symbol
+                and termlists_are_equal(self.subterms, other.subterms))
 
 
 class AggregateCompoundTerm(Term):
@@ -241,10 +240,10 @@ def hash(self):
         raise NotImplementedError()
 
     def is_syntactically_equal(self, other):
-        return self.__class__ is other.__class__ and \
-               self.symbol == other.symbol and \
-               termlists_are_equal(self.bound_vars, other.bound_vars) and \
-               self.subterm.is_syntactically_equal(other.subterm)
+        return (self.__class__ is other.__class__
+                and self.symbol == other.symbol
+                and termlists_are_equal(self.bound_vars, other.bound_vars)
+                and self.subterm.is_syntactically_equal(other.subterm))
 
 
 class IfThenElse(Term):
@@ -293,10 +292,10 @@ def hash(self):
         return hash(('ite', self.condition, termlist_hash(self.subterms)))
 
     def is_syntactically_equal(self, other):
-        return self.__class__ is other.__class__ and \
-               self.symbol == other.symbol and \
-               self.condition.is_syntactically_equal(other.condition) and \
-               termlists_are_equal(self.subterms, other.subterms)
+        return (self.__class__ is other.__class__
+                and self.symbol == other.symbol
+                and self.condition.is_syntactically_equal(other.condition)
+                and termlists_are_equal(self.subterms, other.subterms))
 
 
 def ite(c, t1: Term, t2: Term):
diff --git a/src/tarski/syntax/transform/cnf.py b/src/tarski/syntax/transform/cnf.py
index 807cc01d..487892a3 100644
--- a/src/tarski/syntax/transform/cnf.py
+++ b/src/tarski/syntax/transform/cnf.py
@@ -103,4 +103,3 @@ def to_conjunctive_normal_form(lang, phi, do_copy=True):
 def to_conjunctive_normal_form_clauses(lang, phi, do_copy=True):
     trans = CNFTransformation(lang, phi, do_copy)
     return trans.convert_to_clause_list()
-
diff --git a/src/tarski/syntax/transform/neg_builtin.py b/src/tarski/syntax/transform/neg_builtin.py
index b869f039..c532acf9 100644
--- a/src/tarski/syntax/transform/neg_builtin.py
+++ b/src/tarski/syntax/transform/neg_builtin.py
@@ -30,7 +30,7 @@ def _convert(self, phi):
                 return phi
 
             else:
-                assert phi.connective == Connective.And or phi.connective == Connective.Or
+                assert phi.connective in (Connective.And, Connective.Or)
                 new_sub = [self._convert(phi.subformulas[0]),
                            self._convert(phi.subformulas[1])]
                 phi.subformulas = tuple(new_sub)
diff --git a/src/tarski/syntax/transform/quantifier_elimination.py b/src/tarski/syntax/transform/quantifier_elimination.py
index be8b4f5a..f79ee892 100644
--- a/src/tarski/syntax/transform/quantifier_elimination.py
+++ b/src/tarski/syntax/transform/quantifier_elimination.py
@@ -58,7 +58,8 @@ def _recurse(self, phi):
         return phi
 
     def _expand(self, phi: QuantifiedFormula, creator):
-        from ...grounding.naive import instantiation  # pylint: disable=import-outside-toplevel  # Avoiding circular references
+        # Avoiding circular references in the import:
+        from ...grounding.naive import instantiation  # pylint: disable=import-outside-toplevel
         card, syms, substs = instantiation.enumerate_groundings(phi.variables)
         if card == 0:
             raise TransformationError("quantifier elimination", phi, "No constants were defined!")
diff --git a/src/tarski/syntax/transform/substitutions.py b/src/tarski/syntax/transform/substitutions.py
index 74ea562e..39bdcec4 100644
--- a/src/tarski/syntax/transform/substitutions.py
+++ b/src/tarski/syntax/transform/substitutions.py
@@ -2,7 +2,7 @@
 import itertools
 from typing import List
 
-from multipledispatch import dispatch
+from multipledispatch import dispatch  # type: ignore
 
 from ..symrefs import symref
 from ..terms import Variable
diff --git a/src/tarski/syntax/visitors.py b/src/tarski/syntax/visitors.py
index 644efb9e..e35cb02b 100644
--- a/src/tarski/syntax/visitors.py
+++ b/src/tarski/syntax/visitors.py
@@ -1,5 +1,6 @@
 
-from ..syntax import symref, CompoundFormula, QuantifiedFormula, Atom, CompoundTerm, Variable
+from tarski.syntax import symref, QuantifiedFormula, CompoundTerm, Variable, CompoundFormula, Atom
+from tarski.syntax.formulas import is_eq_atom
 
 
 class MatchExpression:
@@ -78,3 +79,17 @@ def visit(self, phi):
 
         elif isinstance(phi, (Atom, CompoundTerm)):
             _ = [self.visit(f) for f in phi.subterms]
+
+
+class CollectEqualityAtoms:
+    """Collects all equality atoms in given formula"""
+    def __init__(self):
+        self.atoms = set()
+
+    def visit(self, phi):
+        if isinstance(phi, CompoundFormula):
+            _ = [self.visit(f) for f in phi.subformulas]
+        elif isinstance(phi, Atom):
+            if is_eq_atom(phi):
+                self.atoms.add(symref(phi))
+        return
diff --git a/src/tarski/utils/algorithms.py b/src/tarski/utils/algorithms.py
index 48966f10..3942237a 100644
--- a/src/tarski/utils/algorithms.py
+++ b/src/tarski/utils/algorithms.py
@@ -38,10 +38,10 @@ def compute_min_distance(c1s, relation, c2s):
     # Perform breadth-first search
     while queue:
         s = queue.pop()
-        dist_through_s = min_distances[s]+1
+        dist_through_s = min_distances[s] + 1
         for t in adjacencies[s]:
             if dist_through_s < min_distances[t]:
-                min_distances[t] = min_distances[s]+1
+                min_distances[t] = min_distances[s] + 1
                 queue.appendleft(t)
 
     # Return the minimum distance
diff --git a/src/tarski/utils/command.py b/src/tarski/utils/command.py
index 1a790693..5956535a 100644
--- a/src/tarski/utils/command.py
+++ b/src/tarski/utils/command.py
@@ -12,7 +12,7 @@
 def count_file_lines(filename):  # Might be a bit faster with a call to "wc -l"
     """ Count the number of lines in a given file """
     i = 0
-    with open(filename) as f:
+    with open(filename, encoding='utf8') as f:
         for i, _ in enumerate(f, 1):
             pass
     return i
@@ -25,7 +25,7 @@ def remove_duplicate_lines(filename):
 
 def read_file(filename):
     """ Read a file, line by line, ignoring end-of-line characters"""
-    with open(filename) as f:
+    with open(filename, encoding='utf8') as f:
         for line in f:
             yield line.rstrip('\n')
 
@@ -33,10 +33,10 @@ def read_file(filename):
 def execute(command, **kwargs):
     stdout, stderr = kwargs.get("stdout", None), kwargs.get("stderr", None)
     if isinstance(stdout, str):
-        stdout = open(stdout, 'w')
+        stdout = open(stdout, 'w', encoding='utf8')  # pylint: disable=consider-using-with
 
     if isinstance(stderr, str):
-        stderr = open(stderr, 'w')
+        stderr = open(stderr, 'w', encoding='utf8')  # pylint: disable=consider-using-with
 
     cwd = kwargs["cwd"] if "cwd" in kwargs else os.getcwd()
 
@@ -67,48 +67,3 @@ def silentremove(filename):
     except OSError as e:
         if e.errno != errno.ENOENT:  # errno.ENOENT = no such file or directory
             raise  # re-raise exception if a different error occured
-
-
-libc = ctypes.CDLL(None)
-if sys.platform == "darwin":
-    stdout_name = '__stdoutp'
-else:
-    stdout_name = 'stdout'
-c_stdout = ctypes.c_void_p.in_dll(libc, stdout_name)
-
-
-@contextmanager
-def stdout_redirector(stream):
-    # From: https://eli.thegreenplace.net/2015/redirecting-all-kinds-of-stdout-in-python/
-    # The original fd stdout points to. Usually 1 on POSIX systems.
-    original_stdout_fd = sys.stdout.fileno()
-
-    def _redirect_stdout(to_fd):
-        """Redirect stdout to the given file descriptor."""
-        # Flush the C-level buffer stdout
-        libc.fflush(c_stdout)
-        # Flush and close sys.stdout - also closes the file descriptor (fd)
-        sys.stdout.close()
-        # Make original_stdout_fd point to the same file as to_fd
-        os.dup2(to_fd, original_stdout_fd)
-        # Create a new sys.stdout that points to the redirected fd
-        sys.stdout = io.TextIOWrapper(os.fdopen(original_stdout_fd, 'wb'))
-
-    # Save a copy of the original stdout fd in saved_stdout_fd
-    saved_stdout_fd = os.dup(original_stdout_fd)
-    tfile = None
-    try:
-        # Create a temporary file and redirect stdout to it
-        tfile = tempfile.TemporaryFile(mode='w+b')
-        _redirect_stdout(tfile.fileno())
-        # Yield to caller, then redirect stdout back to the saved fd
-        yield
-        _redirect_stdout(saved_stdout_fd)
-        # Copy contents of temporary file to the given stream
-        tfile.flush()
-        tfile.seek(0, io.SEEK_SET)
-        stream.write(tfile.read())
-    finally:
-        if tfile:
-            tfile.close()
-        os.close(saved_stdout_fd)
diff --git a/src/tarski/utils/helpers.py b/src/tarski/utils/helpers.py
index 67e274e5..6aba3334 100644
--- a/src/tarski/utils/helpers.py
+++ b/src/tarski/utils/helpers.py
@@ -7,6 +7,7 @@ def parse_atom(lang, string):
     """ Parse a string representation of an atom such as "on(b1, b2)" from the given language
     and return a Tarski atom object. """
     parts = string.rstrip(')').replace('(', ',').split(',')
+    parts = [part.strip() for part in parts]
     pred = lang.get(parts[0])
     args = [lang.get(o) for o in parts[1:]]
     return pred(*args)
diff --git a/src/tarski/utils/resources.py b/src/tarski/utils/resources.py
index 4d50b572..d205eac0 100644
--- a/src/tarski/utils/resources.py
+++ b/src/tarski/utils/resources.py
@@ -12,7 +12,7 @@ def get_mem_usage():
     """ Return the memory usage as reported by psutil, or None, if the platform
     does not support psutil, or """
     try:
-        import psutil
+        import psutil  # pylint: disable=import-outside-toplevel
     except ImportError:
         return None
     return psutil.Process().memory_info().rss
@@ -36,8 +36,8 @@ def __str__(self):
             # self.start_mem is None whenever the underlying platform could not import the psutil module,
             # in which case at the moment we're happy simply not to show memory consumption info.
             current = get_mem_usage()
-            current_in_mb = current / (1024*1024)
-            rss_in_mb = (current - self.start_mem) / (1024*1024)
+            current_in_mb = current / (1024 * 1024)
+            rss_in_mb = (current - self.start_mem) / (1024 * 1024)
 
         return "[%.2fs CPU, %.2fs wall-clock, diff: %.2fMB, curr:  %.2fMB]" % (
             self._clock() - self.start_clock,
diff --git a/src/tarski/version.py b/src/tarski/version.py
index 4720c1b3..dca2f1a0 100644
--- a/src/tarski/version.py
+++ b/src/tarski/version.py
@@ -1,3 +1,3 @@
 
-__version_info__ = (0, 7, 0)
+__version_info__ = (0, 8, 0)
 __version__ = '.'.join(map(str, __version_info__))
diff --git a/tox.ini b/tox.ini
index 06faf5ba..cdaac607 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,22 +1,17 @@
-;; Examples of other tox.ini configuration files
-;; - https://github.com/Yelp/paasta/blob/master/tox.ini
-
-
-[tox]
-# Don't care too much about these, as they will be overriden by Travis
-envlist = py37
-
-
 [testenv]
 passenv = PYTHONPATH FSBENCHMARKS DOWNWARD_BENCHMARKS
-deps= pytest
 extras =
     arithmetic
     rddl
+
+[testenv:pytest]
+deps= pytest
 commands= pytest
 
 [testenv:mypy]
 basepython = {env:TOX_LATEST_PYTHON:python3}
+extras =
+    stubs
 deps=mypy
 setenv =
     MYPYPATH = {toxinidir}/src
@@ -26,16 +21,14 @@ commands=
     {toxinidir}/scripts/run-mypy
 
 
-[testenv:pycoverage]
-passenv = PYTHONPATH FSBENCHMARKS DOWNWARD_BENCHMARKS CODECOV_TOKEN CI TRAVIS TRAVIS_*
-whitelist_externals = /bin/bash
+[testenv:coverage]
+passenv = PYTHONPATH FSBENCHMARKS DOWNWARD_BENCHMARKS CI
 deps=
     pytest
     pytest-cov
 commands=
     # Report both on screen and generate the xml file
-    pytest --cov --cov-report term --cov-report xml
-    {toxinidir}/scripts/report-coverage
+    pytest --cov-config=tox.ini --cov --cov-report term --cov-report xml
 
 
 [testenv:antlrgrammars]
@@ -44,14 +37,27 @@ commands = {toxinidir}/scripts/build-grammars
 
 
 [testenv:pylint]
+# We avoid the installation of extras for this tox environment, as it appears
+# that tox cannot handle URL-based package specs such as the one we're using for
+# pyrddl at the moment
+extras =
+    parsegen
+    gringo
 deps =
     setuptools
     pylint
-    numpy
-    pyrddl
-    psutil
 commands = {toxinidir}/scripts/lint
 
+
+[testenv:flake8]
+# We avoid the installation of extras for this tox environment, as it appears
+# that tox cannot handle URL-based package specs such as the one we're using for
+# pyrddl at the moment
+deps =
+    flake8
+commands = flake8 src/
+
+
 [testenv:black]
 basepython = {env:TOX_LATEST_PYTHON:python3}
 skipsdist = true
@@ -70,7 +76,7 @@ commands = {toxinidir}/scripts/build-docs
 ### Some coverage.py configuration options  ###
 [coverage:paths]
 source =
-   src
+   src/tarski
    */site-packages/tarski
 
 
@@ -78,12 +84,27 @@ source =
 branch = true
 source =
     tarski
-    tests
 
 [coverage:report]
 ; show_missing = true
 precision = 2
 
+
+[flake8]
+max-line-length = 120
+ignore =
+    # Let's deal with "unused imports" once we move to Python 3.7 and can do lazy importing
+    F401,
+    # The preferred style now is as governed by W504, see https://www.flake8rules.com/rules/W503.html
+    W503,
+
+exclude =
+    # Files auto-generated by ANTLR
+    src/tarski/io/_fstrips/parser
+
+    # Work in progress, full of false positives. TODO Reenable this soon.
+    src/tarski/io/pddl
+
 ### mypy-related configuration options ###
 # https://mypy.readthedocs.io/en/latest/config_file.html
 [mypy]
@@ -91,6 +112,9 @@ precision = 2
 [mypy-tarski.io._fstrips.parser.*]
 ignore_errors = True
 
+[mypy-tarski.io.pddl.parsetab.*]
+ignore_errors = True
+
 [mypy-tarski.rddl.task]
 ignore_errors = True
 
@@ -100,7 +124,7 @@ ignore_errors = True
 [mypy-scipy.*]
 ignore_missing_imports = True
 
-[mypy-numpy]
+[mypy-numpy.*]
 ignore_missing_imports = True
 
 [mypy-antlr4.*]
@@ -108,4 +132,3 @@ ignore_missing_imports = True
 
 [mypy-pyrddl.*]
 ignore_missing_imports = True
-
diff --git a/utils/parsers/grammars/fstrips.g4 b/utils/parsers/grammars/fstrips.g4
index 0834ba54..532d52ff 100644
--- a/utils/parsers/grammars/fstrips.g4
+++ b/utils/parsers/grammars/fstrips.g4
@@ -112,7 +112,7 @@ name_list_with_type
 	
 // A possibly_typed_type_list is different from a possibly_typed_name_list in that it can include 'object'
 // in the *untyped* part of the list (note that 'object' will get tokenized as an OBJECT_T even if it 
-// is also a NAME simply because of the priority of OBJECT_T
+// is also a NAME simply because of the priority of OBJECT_T)
 possibly_typed_type_list
     : (NAME | OBJECT_T)*                       # UntypedTypenameList
     // If there is a mixture of names with and without types,


Test patch:
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
new file mode 100644
index 00000000..62e28af1
--- /dev/null
+++ b/.github/workflows/unit-tests.yml
@@ -0,0 +1,107 @@
+name: Tests
+
+on:
+  # The master branch is protected against direct pushes.
+  # Let's run the tests on all PRs and pushes
+  pull_request:
+    branches: [ master, devel ]
+    # See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
+    types: [opened, reopened]
+  push:
+
+env:
+  DOWNWARD_BENCHMARKS: "/tmp/downward-benchmarks"
+  FSBENCHMARKS: "/tmp/fs-benchmarks"
+
+jobs:
+  unit_tests:
+    strategy:
+      # Keep running all parameter combinations even if one fails
+      fail-fast: false
+
+      matrix:
+        # Currently, we only test on ubuntu, would be good to extend to macos-latest, windows-latest,
+        # but this requires a bit of extra work to get the dependencies installed there too
+        os: [ubuntu-latest]
+
+        # We run unit tests on all currently active Python releases, see https://www.python.org/downloads/.
+        # GH-hosted versions for the Ubuntu image are listed here:
+        # https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md
+        # We also run tests in the latest pre-release version. Available pre-releases are listed here:
+        # https://github.com/actions/python-versions/releases
+        # Note that for the pre-release, we set the `experimental` tag, and hence the `continue-on-error` flag, to true;
+        # this way, failuares are treated as a warning and don’t fail the whole workflow. This is sometimes referred to
+        # as a "shadow CI job".
+        # pypy is currently disabled, as it takes a long time to run (>20mins)
+#        python-version: ['3.7', '3.8', '3.9', '3.10', 'pypy-3.9']
+        python-version: ['3.7', '3.8', '3.9', '3.10']
+        experimental: [false]
+
+        # I'm temporarily disabling the build on the 3.11 alpha, as the installation aims at buiilding the scipy wheel,
+        # which fails spectacularly with a 10K lines log of Fortran compilation errors.
+        # Let's wait a bit until a binary wheel is available
+#        include:
+#          -  os: ubuntu-latest
+#             python-version: '3.11.0-alpha.5'
+#             experimental: true
+
+    runs-on: ${{ matrix.os }}
+    continue-on-error: ${{ matrix.experimental }}
+
+    steps:
+      - name: Clone repo
+        uses: actions/checkout@v2
+
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Install dependencies
+        # Install all dependencies, then get some benchmarks that we'll use to run the tests.
+        # Curl below is necessary to download the benchmarks
+        run: |
+          sudo apt-get install -y --no-install-recommends gringo curl
+          python -m pip install --upgrade tox pip setuptools wheel
+          gringo --version
+          tox --version
+          ./scripts/get-benchmarks
+
+      - name: Run tests (with continue-on-error = ${{ matrix.experimental }})
+        run: |
+          tox -v -e pytest
+
+  style:
+    strategy:
+      # Keep running all parameter combinations even if one fails
+      fail-fast: false
+
+      # We run style tests only on the latest active Python releases, see https://www.python.org/downloads/.
+      matrix:
+        os: [ubuntu-latest]
+        python-version: ['3.10']
+        toxenv: ['docs', 'antlrgrammars', 'pylint', 'flake8', 'mypy']
+
+    runs-on: ${{ matrix.os }}
+    steps:
+      - name: Clone repo
+        uses: actions/checkout@v2
+
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      # Gringo and pandoc required only for the documentation tox environment
+      # Pandoc is installed as an APT package as recommended in the installation instructions.
+      - name: Install Gringo
+        if: matrix.toxenv == 'docs'
+        run: sudo apt-get install -y --no-install-recommends gringo pandoc && gringo --version
+
+      - name: Install tox and packaging dependencies
+        run: |
+          python -m pip install --upgrade tox pip setuptools wheel
+          tox --version
+
+      - name: Run tox environment ${{ matrix.toxenv }}
+        run: tox -v -e ${{ matrix.toxenv }}
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index d9a81f59..00000000
--- a/pytest.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[pytest]
-# Logs will be shown for failed test:
-log_format = %(asctime)s %(levelname)s %(message)s
-log_date_format = %Y-%m-%d %H:%M:%S
-addopts = -rsxX -q
\ No newline at end of file
diff --git a/tests/data/__init__.py b/tests/data/__init__.py
new file mode 100644
index 00000000..8b103ce3
--- /dev/null
+++ b/tests/data/__init__.py
@@ -0,0 +1,8 @@
+from pathlib import Path
+
+HERE = Path(__file__).resolve().parent
+
+
+def resolve_path(filename):
+    """ Return the given filename interpreted as relative to the data directory in the test distribution. """
+    return (HERE / filename).absolute()
diff --git a/tests/data/pddl/derived_predicates/min_cut/domain.pddl b/tests/data/pddl/derived_predicates/min_cut/domain.pddl
new file mode 100644
index 00000000..a37fd1d5
--- /dev/null
+++ b/tests/data/pddl/derived_predicates/min_cut/domain.pddl
@@ -0,0 +1,46 @@
+;; https://github.com/dosydon/axiom_benchmarks/blob/master/mincut/domain.pddl
+
+(define (domain min-cut)
+    (:requirements :strips :typing :negative-preconditions :derived-predicates)
+
+    (:types node edge block)
+
+    (:predicates
+    ;; static
+    (adjacent ?e - edge ?f - edge)
+    (edge-from ?e - edge ?n - node)
+    (edge-to ?e - edge ?n - node)
+    (source-node ?n - node)
+    ;; derived:
+    (blocked ?e - edge)
+    (reachable-node ?n - node)
+    (reachable-edge ?e - edge)
+    (isolated ?n - node)
+    )
+
+    (:functions
+    ;; primary state variables:
+    (at ?b - block) - edge
+    )
+
+    (:derived (blocked ?e - edge)
+        (exists (?b - block) (= (at ?b) ?e)))
+
+    (:derived (reachable-node ?n - node) (source-node ?n))
+    (:derived (reachable-node ?n - node)
+        (exists (?e - edge) (and (edge-to ?e ?n)
+                     (not (blocked ?e))
+                     (reachable-edge ?e))))
+    (:derived (reachable-edge ?e - edge)
+        (exists (?n - node) (and (edge-from ?e ?n)
+                     (reachable-node ?n))))
+    (:derived (isolated ?n - node) (not (reachable-node ?n)))
+
+    (:action move
+    :parameters (?b - block ?from - edge ?to - edge)
+    :precondition (and (adjacent ?from ?to)
+              (= (at ?b) ?from))
+    :effect (assign (at ?b) ?to)
+    )
+
+)
\ No newline at end of file
diff --git a/tests/data/pddl/derived_predicates/min_cut/p00.pddl b/tests/data/pddl/derived_predicates/min_cut/p00.pddl
new file mode 100644
index 00000000..7005ca20
--- /dev/null
+++ b/tests/data/pddl/derived_predicates/min_cut/p00.pddl
@@ -0,0 +1,294 @@
+;; cut = [(0,1),(0,10),(0,11)]
+(define (problem min-cut-12-3-99379962)
+ (:domain min-cut)
+ (:objects
+  n0 - node
+  n1 - node
+  n2 - node
+  n3 - node
+  n4 - node
+  n5 - node
+  n6 - node
+  n7 - node
+  n8 - node
+  n9 - node
+  n10 - node
+  n11 - node
+  e-0-1 - edge
+  e-0-10 - edge
+  e-0-11 - edge
+  e-1-0 - edge
+  e-1-2 - edge
+  e-1-6 - edge
+  e-1-7 - edge
+  e-2-1 - edge
+  e-2-3 - edge
+  e-3-2 - edge
+  e-3-8 - edge
+  e-4-5 - edge
+  e-4-7 - edge
+  e-4-11 - edge
+  e-5-4 - edge
+  e-5-6 - edge
+  e-5-7 - edge
+  e-5-8 - edge
+  e-6-1 - edge
+  e-6-5 - edge
+  e-6-7 - edge
+  e-6-10 - edge
+  e-7-1 - edge
+  e-7-4 - edge
+  e-7-5 - edge
+  e-7-6 - edge
+  e-7-10 - edge
+  e-8-3 - edge
+  e-8-5 - edge
+  e-8-9 - edge
+  e-9-8 - edge
+  e-9-10 - edge
+  e-10-0 - edge
+  e-10-6 - edge
+  e-10-7 - edge
+  e-10-9 - edge
+  e-10-11 - edge
+  e-11-0 - edge
+  e-11-4 - edge
+  e-11-10 - edge
+  b0 - block
+  b1 - block
+  b2 - block
+  )
+ (:init
+  (edge-from e-0-1 n0)
+  (edge-to e-0-1 n1)
+  (adjacent e-0-1 e-1-0)
+  (adjacent e-0-1 e-1-2)
+  (adjacent e-0-1 e-1-6)
+  (adjacent e-0-1 e-1-7)
+  (edge-from e-0-10 n0)
+  (edge-to e-0-10 n10)
+  (adjacent e-0-10 e-10-0)
+  (adjacent e-0-10 e-10-6)
+  (adjacent e-0-10 e-10-7)
+  (adjacent e-0-10 e-10-9)
+  (adjacent e-0-10 e-10-11)
+  (edge-from e-0-11 n0)
+  (edge-to e-0-11 n11)
+  (adjacent e-0-11 e-11-0)
+  (adjacent e-0-11 e-11-4)
+  (adjacent e-0-11 e-11-10)
+  (edge-from e-1-0 n1)
+  (edge-to e-1-0 n0)
+  (adjacent e-1-0 e-0-1)
+  (adjacent e-1-0 e-0-10)
+  (adjacent e-1-0 e-0-11)
+  (edge-from e-1-2 n1)
+  (edge-to e-1-2 n2)
+  (adjacent e-1-2 e-2-1)
+  (adjacent e-1-2 e-2-3)
+  (edge-from e-1-6 n1)
+  (edge-to e-1-6 n6)
+  (adjacent e-1-6 e-6-1)
+  (adjacent e-1-6 e-6-5)
+  (adjacent e-1-6 e-6-7)
+  (adjacent e-1-6 e-6-10)
+  (edge-from e-1-7 n1)
+  (edge-to e-1-7 n7)
+  (adjacent e-1-7 e-7-1)
+  (adjacent e-1-7 e-7-4)
+  (adjacent e-1-7 e-7-5)
+  (adjacent e-1-7 e-7-6)
+  (adjacent e-1-7 e-7-10)
+  (edge-from e-2-1 n2)
+  (edge-to e-2-1 n1)
+  (adjacent e-2-1 e-1-0)
+  (adjacent e-2-1 e-1-2)
+  (adjacent e-2-1 e-1-6)
+  (adjacent e-2-1 e-1-7)
+  (edge-from e-2-3 n2)
+  (edge-to e-2-3 n3)
+  (adjacent e-2-3 e-3-2)
+  (adjacent e-2-3 e-3-8)
+  (edge-from e-3-2 n3)
+  (edge-to e-3-2 n2)
+  (adjacent e-3-2 e-2-1)
+  (adjacent e-3-2 e-2-3)
+  (edge-from e-3-8 n3)
+  (edge-to e-3-8 n8)
+  (adjacent e-3-8 e-8-3)
+  (adjacent e-3-8 e-8-5)
+  (adjacent e-3-8 e-8-9)
+  (edge-from e-4-5 n4)
+  (edge-to e-4-5 n5)
+  (adjacent e-4-5 e-5-4)
+  (adjacent e-4-5 e-5-6)
+  (adjacent e-4-5 e-5-7)
+  (adjacent e-4-5 e-5-8)
+  (edge-from e-4-7 n4)
+  (edge-to e-4-7 n7)
+  (adjacent e-4-7 e-7-1)
+  (adjacent e-4-7 e-7-4)
+  (adjacent e-4-7 e-7-5)
+  (adjacent e-4-7 e-7-6)
+  (adjacent e-4-7 e-7-10)
+  (edge-from e-4-11 n4)
+  (edge-to e-4-11 n11)
+  (adjacent e-4-11 e-11-0)
+  (adjacent e-4-11 e-11-4)
+  (adjacent e-4-11 e-11-10)
+  (edge-from e-5-4 n5)
+  (edge-to e-5-4 n4)
+  (adjacent e-5-4 e-4-5)
+  (adjacent e-5-4 e-4-7)
+  (adjacent e-5-4 e-4-11)
+  (edge-from e-5-6 n5)
+  (edge-to e-5-6 n6)
+  (adjacent e-5-6 e-6-1)
+  (adjacent e-5-6 e-6-5)
+  (adjacent e-5-6 e-6-7)
+  (adjacent e-5-6 e-6-10)
+  (edge-from e-5-7 n5)
+  (edge-to e-5-7 n7)
+  (adjacent e-5-7 e-7-1)
+  (adjacent e-5-7 e-7-4)
+  (adjacent e-5-7 e-7-5)
+  (adjacent e-5-7 e-7-6)
+  (adjacent e-5-7 e-7-10)
+  (edge-from e-5-8 n5)
+  (edge-to e-5-8 n8)
+  (adjacent e-5-8 e-8-3)
+  (adjacent e-5-8 e-8-5)
+  (adjacent e-5-8 e-8-9)
+  (edge-from e-6-1 n6)
+  (edge-to e-6-1 n1)
+  (adjacent e-6-1 e-1-0)
+  (adjacent e-6-1 e-1-2)
+  (adjacent e-6-1 e-1-6)
+  (adjacent e-6-1 e-1-7)
+  (edge-from e-6-5 n6)
+  (edge-to e-6-5 n5)
+  (adjacent e-6-5 e-5-4)
+  (adjacent e-6-5 e-5-6)
+  (adjacent e-6-5 e-5-7)
+  (adjacent e-6-5 e-5-8)
+  (edge-from e-6-7 n6)
+  (edge-to e-6-7 n7)
+  (adjacent e-6-7 e-7-1)
+  (adjacent e-6-7 e-7-4)
+  (adjacent e-6-7 e-7-5)
+  (adjacent e-6-7 e-7-6)
+  (adjacent e-6-7 e-7-10)
+  (edge-from e-6-10 n6)
+  (edge-to e-6-10 n10)
+  (adjacent e-6-10 e-10-0)
+  (adjacent e-6-10 e-10-6)
+  (adjacent e-6-10 e-10-7)
+  (adjacent e-6-10 e-10-9)
+  (adjacent e-6-10 e-10-11)
+  (edge-from e-7-1 n7)
+  (edge-to e-7-1 n1)
+  (adjacent e-7-1 e-1-0)
+  (adjacent e-7-1 e-1-2)
+  (adjacent e-7-1 e-1-6)
+  (adjacent e-7-1 e-1-7)
+  (edge-from e-7-4 n7)
+  (edge-to e-7-4 n4)
+  (adjacent e-7-4 e-4-5)
+  (adjacent e-7-4 e-4-7)
+  (adjacent e-7-4 e-4-11)
+  (edge-from e-7-5 n7)
+  (edge-to e-7-5 n5)
+  (adjacent e-7-5 e-5-4)
+  (adjacent e-7-5 e-5-6)
+  (adjacent e-7-5 e-5-7)
+  (adjacent e-7-5 e-5-8)
+  (edge-from e-7-6 n7)
+  (edge-to e-7-6 n6)
+  (adjacent e-7-6 e-6-1)
+  (adjacent e-7-6 e-6-5)
+  (adjacent e-7-6 e-6-7)
+  (adjacent e-7-6 e-6-10)
+  (edge-from e-7-10 n7)
+  (edge-to e-7-10 n10)
+  (adjacent e-7-10 e-10-0)
+  (adjacent e-7-10 e-10-6)
+  (adjacent e-7-10 e-10-7)
+  (adjacent e-7-10 e-10-9)
+  (adjacent e-7-10 e-10-11)
+  (edge-from e-8-3 n8)
+  (edge-to e-8-3 n3)
+  (adjacent e-8-3 e-3-2)
+  (adjacent e-8-3 e-3-8)
+  (edge-from e-8-5 n8)
+  (edge-to e-8-5 n5)
+  (adjacent e-8-5 e-5-4)
+  (adjacent e-8-5 e-5-6)
+  (adjacent e-8-5 e-5-7)
+  (adjacent e-8-5 e-5-8)
+  (edge-from e-8-9 n8)
+  (edge-to e-8-9 n9)
+  (adjacent e-8-9 e-9-8)
+  (adjacent e-8-9 e-9-10)
+  (edge-from e-9-8 n9)
+  (edge-to e-9-8 n8)
+  (adjacent e-9-8 e-8-3)
+  (adjacent e-9-8 e-8-5)
+  (adjacent e-9-8 e-8-9)
+  (edge-from e-9-10 n9)
+  (edge-to e-9-10 n10)
+  (adjacent e-9-10 e-10-0)
+  (adjacent e-9-10 e-10-6)
+  (adjacent e-9-10 e-10-7)
+  (adjacent e-9-10 e-10-9)
+  (adjacent e-9-10 e-10-11)
+  (edge-from e-10-0 n10)
+  (edge-to e-10-0 n0)
+  (adjacent e-10-0 e-0-1)
+  (adjacent e-10-0 e-0-10)
+  (adjacent e-10-0 e-0-11)
+  (edge-from e-10-6 n10)
+  (edge-to e-10-6 n6)
+  (adjacent e-10-6 e-6-1)
+  (adjacent e-10-6 e-6-5)
+  (adjacent e-10-6 e-6-7)
+  (adjacent e-10-6 e-6-10)
+  (edge-from e-10-7 n10)
+  (edge-to e-10-7 n7)
+  (adjacent e-10-7 e-7-1)
+  (adjacent e-10-7 e-7-4)
+  (adjacent e-10-7 e-7-5)
+  (adjacent e-10-7 e-7-6)
+  (adjacent e-10-7 e-7-10)
+  (edge-from e-10-9 n10)
+  (edge-to e-10-9 n9)
+  (adjacent e-10-9 e-9-8)
+  (adjacent e-10-9 e-9-10)
+  (edge-from e-10-11 n10)
+  (edge-to e-10-11 n11)
+  (adjacent e-10-11 e-11-0)
+  (adjacent e-10-11 e-11-4)
+  (adjacent e-10-11 e-11-10)
+  (edge-from e-11-0 n11)
+  (edge-to e-11-0 n0)
+  (adjacent e-11-0 e-0-1)
+  (adjacent e-11-0 e-0-10)
+  (adjacent e-11-0 e-0-11)
+  (edge-from e-11-4 n11)
+  (edge-to e-11-4 n4)
+  (adjacent e-11-4 e-4-5)
+  (adjacent e-11-4 e-4-7)
+  (adjacent e-11-4 e-4-11)
+  (edge-from e-11-10 n11)
+  (edge-to e-11-10 n10)
+  (adjacent e-11-10 e-10-0)
+  (adjacent e-11-10 e-10-6)
+  (adjacent e-11-10 e-10-7)
+  (adjacent e-11-10 e-10-9)
+  (adjacent e-11-10 e-10-11)
+  (= (at b0) e-1-2)
+  (= (at b1) e-3-8)
+  (= (at b2) e-5-7)
+  (source-node n0)
+  )
+ (:goal (isolated n6))
+ )
\ No newline at end of file
diff --git a/tests/data/pddl/issue114/domain.pddl b/tests/data/pddl/issue114/domain.pddl
new file mode 100755
index 00000000..3ad99304
--- /dev/null
+++ b/tests/data/pddl/issue114/domain.pddl
@@ -0,0 +1,60 @@
+;; Enrico Scala (enricos83@gmail.com) and Miquel Ramirez (miquel.ramirez@gmail.com)
+(define (domain sailing)
+     (:types
+          boat -object
+          person -object
+     )
+     (:predicates
+          (saved ?t -person)
+     )
+     (:functions
+          (x ?b -boat)
+          (y ?b -boat)
+          (d ?t -person)
+     )
+     (:action go_north_east
+          :parameters (?b -boat)
+          :precondition ()
+          :effect (and(increase (x ?b) 1.5) (increase (y ?b) 1.5))
+     )
+     (:action go_north_west
+          :parameters (?b -boat)
+          :precondition ()
+          :effect (and(decrease (x ?b) 1.5) (increase (y ?b) 1.5))
+     )
+     (:action go_est
+          :parameters (?b -boat)
+          :precondition ()
+          :effect (and(increase (x ?b) 3))
+     )
+     (:action go_west
+          :parameters (?b -boat)
+          :precondition ()
+          :effect (and(decrease (x ?b) 3))
+     )
+     (:action go_south_west
+          :parameters(?b -boat)
+          :precondition ()
+          :effect (and(increase (x ?b) 2) (decrease (y ?b) 2))
+     )
+     (:action go_south_east
+          :parameters(?b -boat)
+          :precondition ()
+          :effect (and(decrease (x ?b) 2) (decrease (y ?b) 2))
+     )
+     (:action go_south
+          :parameters(?b -boat)
+          :precondition ()
+          :effect (and (decrease (y ?b) 2))
+     )
+     (:action save_person
+          :parameters(?b -boat ?t -person)
+          :precondition ( and (>= (+ (x ?b) (y ?b)) (d ?t))
+               (>= (- (y ?b) (x ?b)) (d ?t))
+               (<= (+ (x ?b) (y ?b)) (+ (d ?t) 25))
+               (<= (- (y ?b) (x ?b)) (+ (d ?t) 25))
+          )
+          :effect (and(saved ?t))
+     )
+
+)
diff --git a/tests/data/pddl/issue114/problem.pddl b/tests/data/pddl/issue114/problem.pddl
new file mode 100755
index 00000000..464bd1f2
--- /dev/null
+++ b/tests/data/pddl/issue114/problem.pddl
@@ -0,0 +1,29 @@
+;; Enrico Scala (enricos83@gmail.com) and Miquel Ramirez (miquel.ramirez@gmail.com)
+;;Setting seed to 1229
+(define (problem instance_2_3_1229)
+
+	(:domain sailing)
+
+	(:objects
+		b0 b1 - boat
+		p0 p1 - person
+	)
+
+	(:init
+		(= (x b0) -7)
+		(= (y b0) 0)
+		(= (x b1) -2)
+		(= (y b1) 0)
+
+		(= (d p0) -20)
+		(= (d p1) -38)
+	)
+
+	(:goal
+		(and
+			(saved p0)
+			(saved p1)
+
+		)
+	)
+)
\ No newline at end of file
diff --git a/tests/data/pddl/temporal/ipc08/elevators/p01-domain.pddl b/tests/data/pddl/temporal/ipc08/elevators/p01-domain.pddl
new file mode 100644
index 00000000..a461c648
--- /dev/null
+++ b/tests/data/pddl/temporal/ipc08/elevators/p01-domain.pddl
@@ -0,0 +1,60 @@
+(define (domain elevators-time-numeric)
+  (:requirements :typing :durative-actions :numeric-fluents)
+  (:types 	elevator - object 
+			slow-elevator fast-elevator - elevator
+   			passenger - object
+          	floor - object
+         )
+
+(:predicates 
+	(passenger-at ?person - passenger ?floor - floor)
+	(boarded ?person - passenger ?lift - elevator)
+	(lift-at ?lift - elevator ?floor - floor)
+	(reachable-floor ?lift - elevator ?floor - floor)
+	(above ?floor1 - floor ?floor2 - floor)
+)
+
+(:functions 
+            (travel-slow ?f1 - floor ?f2 - floor) - number
+            (travel-fast ?f1 - floor ?f2 - floor) - number 
+			(passengers ?lift - elevator) - number
+			(capacity ?lift - elevator) - number
+)
+
+(:durative-action move-up-slow
+  :parameters (?lift - slow-elevator ?f1 - floor ?f2 - floor )
+  :duration (= ?duration (travel-slow ?f1 ?f2))
+  :condition (and (at start (lift-at ?lift ?f1)) (at start (above ?f1 ?f2)) (at start (reachable-floor ?lift ?f2)) )
+  :effect (and (at end (lift-at ?lift ?f2)) (at start (not (lift-at ?lift ?f1))) ))
+
+(:durative-action move-down-slow
+  :parameters (?lift - slow-elevator ?f1 - floor ?f2 - floor )
+  :duration (= ?duration (travel-slow ?f2 ?f1))
+  :condition (and (at start (lift-at ?lift ?f1)) (at start (above ?f2 ?f1)) (at start (reachable-floor ?lift ?f2)) )
+  :effect (and (at end (lift-at ?lift ?f2)) (at start (not (lift-at ?lift ?f1))) ))
+
+(:durative-action move-up-fast
+  :parameters (?lift - fast-elevator ?f1 - floor ?f2 - floor )
+  :duration (= ?duration (travel-fast ?f1 ?f2))
+  :condition (and (at start (lift-at ?lift ?f1)) (at start (above ?f1 ?f2)) (at start (reachable-floor ?lift ?f2)) )
+  :effect (and (at end (lift-at ?lift ?f2)) (at start (not (lift-at ?lift ?f1))) ))
+
+(:durative-action move-down-fast
+  :parameters (?lift - fast-elevator ?f1 - floor ?f2 - floor )
+  :duration (= ?duration (travel-fast ?f2 ?f1))
+  :condition (and (at start (lift-at ?lift ?f1)) (at start (above ?f2 ?f1)) (at start (reachable-floor ?lift ?f2)) )
+  :effect (and (at end (lift-at ?lift ?f2)) (at start (not (lift-at ?lift ?f1))) ))
+
+(:durative-action board
+  :parameters (?p - passenger ?lift - elevator ?f - floor)
+  :duration (= ?duration 1)
+  :condition (and  (over all (lift-at ?lift ?f)) (at start (passenger-at ?p ?f))  (at start (< (passengers ?lift) (capacity ?lift))) )
+  :effect (and (at start (not (passenger-at ?p ?f))) (at end (boarded ?p ?lift)) (at start (increase (passengers ?lift) 1)) ))
+
+(:durative-action leave 
+  :parameters (?p - passenger ?lift - elevator ?f - floor)
+  :duration (= ?duration 1)
+  :condition (and  (over all (lift-at ?lift ?f)) (at start (boarded ?p ?lift)) )
+  :effect (and (at end (passenger-at ?p ?f)) (at start (not (boarded ?p ?lift))) (at end (decrease (passengers ?lift) 1)) ))
+  
+)
diff --git a/tests/data/pddl/temporal/ipc08/elevators/p01.pddl b/tests/data/pddl/temporal/ipc08/elevators/p01.pddl
new file mode 100644
index 00000000..c92a3858
--- /dev/null
+++ b/tests/data/pddl/temporal/ipc08/elevators/p01.pddl
@@ -0,0 +1,75 @@
+(define (problem elevators-time-p8_4_1)
+(:domain elevators-time-numeric)
+
+(:objects 
+f0 f1 f2 f3 f4 f5 f6 f7 f8  - floor
+p0 p1 p2 p3  - passenger
+fast0 fast1  - fast-elevator
+slow0-0 slow1-0 - slow-elevator
+)
+
+(:init
+(above f0 f1) (above f0 f2) (above f0 f3) (above f0 f4) (above f0 f5) (above f0 f6) (above f0 f7) (above f0 f8) 
+(above f1 f2) (above f1 f3) (above f1 f4) (above f1 f5) (above f1 f6) (above f1 f7) (above f1 f8) 
+(above f2 f3) (above f2 f4) (above f2 f5) (above f2 f6) (above f2 f7) (above f2 f8) 
+(above f3 f4) (above f3 f5) (above f3 f6) (above f3 f7) (above f3 f8) 
+(above f4 f5) (above f4 f6) (above f4 f7) (above f4 f8) 
+(above f5 f6) (above f5 f7) (above f5 f8) 
+(above f6 f7) (above f6 f8) 
+(above f7 f8) 
+
+(lift-at fast0 f8)
+(= (passengers fast0) 0)
+(= (capacity fast0) 3)
+(reachable-floor fast0 f0)(reachable-floor fast0 f2)(reachable-floor fast0 f4)(reachable-floor fast0 f6)(reachable-floor fast0 f8)
+
+(lift-at fast1 f6)
+(= (passengers fast1) 0)
+(= (capacity fast1) 3)
+(reachable-floor fast1 f0)(reachable-floor fast1 f2)(reachable-floor fast1 f4)(reachable-floor fast1 f6)(reachable-floor fast1 f8)
+
+(lift-at slow0-0 f4)
+(= (passengers slow0-0) 0)
+(= (capacity slow0-0) 2)
+(reachable-floor slow0-0 f0)(reachable-floor slow0-0 f1)(reachable-floor slow0-0 f2)(reachable-floor slow0-0 f3)(reachable-floor slow0-0 f4)
+
+(lift-at slow1-0 f4)
+(= (passengers slow1-0) 0)
+(= (capacity slow1-0) 2)
+(reachable-floor slow1-0 f4)(reachable-floor slow1-0 f5)(reachable-floor slow1-0 f6)(reachable-floor slow1-0 f7)(reachable-floor slow1-0 f8)
+
+(passenger-at p0 f3)
+(passenger-at p1 f1)
+(passenger-at p2 f7)
+(passenger-at p3 f0)
+
+(= (travel-slow f0 f1) 12) (= (travel-slow f0 f2) 20) (= (travel-slow f0 f3) 28) (= (travel-slow f0 f4) 36)
+(= (travel-slow f1 f2) 12) (= (travel-slow f1 f3) 20) (= (travel-slow f1 f4) 28) (= (travel-slow f2 f3) 12)
+(= (travel-slow f2 f4) 20) (= (travel-slow f3 f4) 12)
+
+(= (travel-slow f4 f5) 12) (= (travel-slow f4 f6) 20) (= (travel-slow f4 f7) 28) (= (travel-slow f4 f8) 36)
+(= (travel-slow f5 f6) 12) (= (travel-slow f5 f7) 20) (= (travel-slow f5 f8) 28) (= (travel-slow f6 f7) 12)
+(= (travel-slow f6 f8) 20) (= (travel-slow f7 f8) 12)
+
+
+(= (travel-fast f0 f2) 11) (= (travel-fast f0 f4) 13) (= (travel-fast f0 f6) 15) (= (travel-fast f0 f8) 17) 
+
+(= (travel-fast f2 f4) 11) (= (travel-fast f2 f6) 13) (= (travel-fast f2 f8) 15) 
+
+(= (travel-fast f4 f6) 11) (= (travel-fast f4 f8) 13) 
+
+(= (travel-fast f6 f8) 11) 
+
+)
+
+(:goal
+(and
+(passenger-at p0 f4)
+(passenger-at p1 f5)
+(passenger-at p2 f6)
+(passenger-at p3 f2)
+))
+
+(:metric minimize (total-time))
+
+)
diff --git a/tests/data/pddl/temporal/ipc18/airport-temporal-strips/2/domain.pddl b/tests/data/pddl/temporal/ipc18/airport-temporal-strips/2/domain.pddl
new file mode 100644
index 00000000..04993db4
--- /dev/null
+++ b/tests/data/pddl/temporal/ipc18/airport-temporal-strips/2/domain.pddl
@@ -0,0 +1,1125 @@
+;;
+;; PDDL file for the AIPS2000 Planning Competition
+;; based on the data generated by the airport simulator Astras.
+;;
+
+;; Author: Sebastian Trueg thisshouldbethecurrentdateandtime :(
+;; Created with PreInstancerDomainExporter 0.6 by Sebastian Trueg <trueg@informatik.uni-freiburg.de>
+;;
+
+
+(define (domain airport_fixed_structure)
+
+(:requirements :durative-actions :typing)
+
+(:types airplane segment direction airplanetype)
+
+(:constants
+                north
+                south - direction
+
+                light
+                medium
+                heavy - airplanetype
+
+                seg_pp_0_60
+                seg_ppdoor_0_40
+                seg_tww1_0_200
+                seg_twe1_0_200
+                seg_tww2_0_50
+                seg_tww3_0_50
+                seg_tww4_0_50
+                seg_rww_0_50
+                seg_rwtw1_0_10
+                seg_rw_0_400
+                seg_rwe_0_50
+                seg_twe4_0_50
+                seg_rwte1_0_10
+                seg_twe3_0_50
+                seg_twe2_0_50
+                seg_rwte2_0_10
+                seg_rwtw2_0_10 - segment
+
+                airplane_DAEWH - airplane
+)
+
+(:predicates
+                ;; airport information
+                (has-type ?a - airplane ?t - airplanetype)
+                ;; plane a has type t
+                (at-segment ?a - airplane ?s - segment)
+                ;; planes are at segments, ie at their end in driving direction
+                (facing ?a - airplane ?d - direction)
+                ;; planes face into their driving direction
+
+                ;; how the planes affect the airport
+                (occupied ?s - segment)
+                ;; a plane is in here
+                (not_occupied ?s - segment)
+                (blocked ?s - segment ?a - airplane)
+                ;; segment s is blocked if it is endangered by plane p
+                (not_blocked ?s - segment ?a - airplane)
+
+                ;; an airplane may lineup on segment s when facing d
+                (is-start-runway ?s - segment ?d - direction)
+
+                ;; airplane a is starting from runway s
+                (airborne ?a - airplane ?s - segment)
+
+                (is-moving ?a - airplane)
+                (is-pushing ?a - airplane)
+                (is-parked ?a - airplane ?s - segment)
+
+)
+
+(:functions
+                ;; the length of a segment
+                (length ?s - segment)
+
+                ;; the number of engines of an airplane
+                (engines ?a - airplane)
+)
+
+(:durative-action move_seg_pp_0_60_seg_ppdoor_0_40_north_north_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 2)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a north))
+                 (at start (at-segment ?a seg_pp_0_60))
+                 (at start (not_occupied seg_ppdoor_0_40))
+                 (at start (not_blocked seg_ppdoor_0_40 airplane_DAEWH))
+
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_pp_0_60)))
+                 (at end (not_occupied seg_pp_0_60))
+                 (at end (not (at-segment ?a seg_pp_0_60)))
+
+                 (at start (occupied seg_ppdoor_0_40))
+                 (at start (not (not_occupied seg_ppdoor_0_40)))
+                 (at start (blocked seg_ppdoor_0_40 ?a))
+                 (at start (not (not_blocked seg_ppdoor_0_40 ?a)))
+                 (at end (at-segment ?a seg_ppdoor_0_40))
+
+
+                )
+)
+
+(:durative-action move_seg_ppdoor_0_40_seg_tww1_0_200_north_south_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 1)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a north))
+                 (at start (at-segment ?a seg_ppdoor_0_40))
+                 (at start (not_occupied seg_tww1_0_200))
+                 (at start (not_blocked seg_tww1_0_200 airplane_DAEWH))
+
+                 (over all (not_occupied seg_twe1_0_200))
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_ppdoor_0_40)))
+                 (at end (not_occupied seg_ppdoor_0_40))
+                 (at end (not (at-segment ?a seg_ppdoor_0_40)))
+
+                 (at start (occupied seg_tww1_0_200))
+                 (at start (not (not_occupied seg_tww1_0_200)))
+                 (at start (blocked seg_tww1_0_200 ?a))
+                 (at start (not (not_blocked seg_tww1_0_200 ?a)))
+                 (at end (at-segment ?a seg_tww1_0_200))
+                 (at end (not (facing ?a north)))
+                 (at end (facing ?a south))
+
+                 (at end (not (blocked seg_pp_0_60 ?a)))
+                 (at end (not_blocked seg_pp_0_60 ?a))
+
+                 (at end (blocked seg_twe1_0_200 ?a))
+                 (at end (not (not_blocked seg_twe1_0_200 ?a)))
+                )
+)
+
+(:durative-action move_seg_tww1_0_200_seg_twe1_0_200_north_south_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 6)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a north))
+                 (at start (at-segment ?a seg_tww1_0_200))
+                 (at start (not_occupied seg_twe1_0_200))
+                 (at start (not_blocked seg_twe1_0_200 airplane_DAEWH))
+
+                 (over all (not_occupied seg_ppdoor_0_40))
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_tww1_0_200)))
+                 (at end (not_occupied seg_tww1_0_200))
+                 (at end (not (at-segment ?a seg_tww1_0_200)))
+
+                 (at start (occupied seg_twe1_0_200))
+                 (at start (not (not_occupied seg_twe1_0_200)))
+                 (at start (blocked seg_twe1_0_200 ?a))
+                 (at start (not (not_blocked seg_twe1_0_200 ?a)))
+                 (at end (at-segment ?a seg_twe1_0_200))
+                 (at end (not (facing ?a north)))
+                 (at end (facing ?a south))
+
+                 (at end (not (blocked seg_tww2_0_50 ?a)))
+                 (at end (not_blocked seg_tww2_0_50 ?a))
+
+                 (at end (blocked seg_ppdoor_0_40 ?a))
+                 (at end (not (not_blocked seg_ppdoor_0_40 ?a)))
+                )
+)
+
+(:durative-action move_seg_twe1_0_200_seg_twe2_0_50_south_south_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 6)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a south))
+                 (at start (at-segment ?a seg_twe1_0_200))
+                 (at start (not_occupied seg_twe2_0_50))
+                 (at start (not_blocked seg_twe2_0_50 airplane_DAEWH))
+
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_twe1_0_200)))
+                 (at end (not_occupied seg_twe1_0_200))
+                 (at end (not (at-segment ?a seg_twe1_0_200)))
+
+                 (at start (occupied seg_twe2_0_50))
+                 (at start (not (not_occupied seg_twe2_0_50)))
+                 (at start (blocked seg_twe2_0_50 ?a))
+                 (at start (not (not_blocked seg_twe2_0_50 ?a)))
+                 (at end (at-segment ?a seg_twe2_0_50))
+
+                 (at end (not (blocked seg_ppdoor_0_40 ?a)))
+                 (at end (not_blocked seg_ppdoor_0_40 ?a))
+                 (at end (not (blocked seg_tww1_0_200 ?a)))
+                 (at end (not_blocked seg_tww1_0_200 ?a))
+
+                )
+)
+
+(:durative-action move_seg_twe2_0_50_seg_twe3_0_50_south_south_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 1)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a south))
+                 (at start (at-segment ?a seg_twe2_0_50))
+                 (at start (not_occupied seg_twe3_0_50))
+                 (at start (not_blocked seg_twe3_0_50 airplane_DAEWH))
+
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_twe2_0_50)))
+                 (at end (not_occupied seg_twe2_0_50))
+                 (at end (not (at-segment ?a seg_twe2_0_50)))
+
+                 (at start (occupied seg_twe3_0_50))
+                 (at start (not (not_occupied seg_twe3_0_50)))
+                 (at start (blocked seg_twe3_0_50 ?a))
+                 (at start (not (not_blocked seg_twe3_0_50 ?a)))
+                 (at end (at-segment ?a seg_twe3_0_50))
+
+                 (at end (not (blocked seg_twe1_0_200 ?a)))
+                 (at end (not_blocked seg_twe1_0_200 ?a))
+
+                )
+)
+
+(:durative-action move_seg_twe3_0_50_seg_twe4_0_50_south_south_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 1)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a south))
+                 (at start (at-segment ?a seg_twe3_0_50))
+                 (at start (not_occupied seg_twe4_0_50))
+                 (at start (not_blocked seg_twe4_0_50 airplane_DAEWH))
+
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_twe3_0_50)))
+                 (at end (not_occupied seg_twe3_0_50))
+                 (at end (not (at-segment ?a seg_twe3_0_50)))
+
+                 (at start (occupied seg_twe4_0_50))
+                 (at start (not (not_occupied seg_twe4_0_50)))
+                 (at start (blocked seg_twe4_0_50 ?a))
+                 (at start (not (not_blocked seg_twe4_0_50 ?a)))
+                 (at end (at-segment ?a seg_twe4_0_50))
+
+                 (at end (not (blocked seg_twe2_0_50 ?a)))
+                 (at end (not_blocked seg_twe2_0_50 ?a))
+
+                )
+)
+
+(:durative-action move_seg_twe4_0_50_seg_rwe_0_50_south_south_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 1)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a south))
+                 (at start (at-segment ?a seg_twe4_0_50))
+                 (at start (not_occupied seg_rwe_0_50))
+                 (at start (not_blocked seg_rwe_0_50 airplane_DAEWH))
+
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_twe4_0_50)))
+                 (at end (not_occupied seg_twe4_0_50))
+                 (at end (not (at-segment ?a seg_twe4_0_50)))
+
+                 (at start (occupied seg_rwe_0_50))
+                 (at start (not (not_occupied seg_rwe_0_50)))
+                 (at start (blocked seg_rwe_0_50 ?a))
+                 (at start (not (not_blocked seg_rwe_0_50 ?a)))
+                 (at end (at-segment ?a seg_rwe_0_50))
+
+                 (at end (not (blocked seg_twe3_0_50 ?a)))
+                 (at end (not_blocked seg_twe3_0_50 ?a))
+
+                )
+)
+
+(:durative-action move_seg_rwe_0_50_seg_rw_0_400_south_south_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 1)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a south))
+                 (at start (at-segment ?a seg_rwe_0_50))
+                 (at start (not_occupied seg_rw_0_400))
+                 (at start (not_blocked seg_rw_0_400 airplane_DAEWH))
+
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_rwe_0_50)))
+                 (at end (not_occupied seg_rwe_0_50))
+                 (at end (not (at-segment ?a seg_rwe_0_50)))
+
+                 (at start (occupied seg_rw_0_400))
+                 (at start (not (not_occupied seg_rw_0_400)))
+                 (at start (blocked seg_rw_0_400 ?a))
+                 (at start (not (not_blocked seg_rw_0_400 ?a)))
+                 (at end (at-segment ?a seg_rw_0_400))
+
+                 (at end (not (blocked seg_twe4_0_50 ?a)))
+                 (at end (not_blocked seg_twe4_0_50 ?a))
+
+                )
+)
+
+(:durative-action move_seg_rw_0_400_seg_rww_0_50_south_south_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 13)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a south))
+                 (at start (at-segment ?a seg_rw_0_400))
+                 (at start (not_occupied seg_rww_0_50))
+                 (at start (not_blocked seg_rww_0_50 airplane_DAEWH))
+
+                 (over all (not_occupied seg_rwe_0_50))
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_rw_0_400)))
+                 (at end (not_occupied seg_rw_0_400))
+                 (at end (not (at-segment ?a seg_rw_0_400)))
+
+                 (at start (occupied seg_rww_0_50))
+                 (at start (not (not_occupied seg_rww_0_50)))
+                 (at start (blocked seg_rww_0_50 ?a))
+                 (at start (not (not_blocked seg_rww_0_50 ?a)))
+                 (at end (at-segment ?a seg_rww_0_50))
+
+
+                )
+)
+
+(:durative-action move_seg_rww_0_50_seg_tww4_0_50_south_north_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 1)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a south))
+                 (at start (at-segment ?a seg_rww_0_50))
+                 (at start (not_occupied seg_tww4_0_50))
+                 (at start (not_blocked seg_tww4_0_50 airplane_DAEWH))
+
+                 (over all (not_occupied seg_rwtw1_0_10))
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_rww_0_50)))
+                 (at end (not_occupied seg_rww_0_50))
+                 (at end (not (at-segment ?a seg_rww_0_50)))
+
+                 (at start (occupied seg_tww4_0_50))
+                 (at start (not (not_occupied seg_tww4_0_50)))
+                 (at start (blocked seg_tww4_0_50 ?a))
+                 (at start (not (not_blocked seg_tww4_0_50 ?a)))
+                 (at end (at-segment ?a seg_tww4_0_50))
+                 (at end (not (facing ?a south)))
+                 (at end (facing ?a north))
+
+                 (at end (not (blocked seg_rwe_0_50 ?a)))
+                 (at end (not_blocked seg_rwe_0_50 ?a))
+                 (at end (not (blocked seg_rw_0_400 ?a)))
+                 (at end (not_blocked seg_rw_0_400 ?a))
+
+                 (at end (blocked seg_rwtw1_0_10 ?a))
+                 (at end (not (not_blocked seg_rwtw1_0_10 ?a)))
+                )
+)
+
+(:durative-action move_seg_tww4_0_50_seg_tww3_0_50_north_north_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 1)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a north))
+                 (at start (at-segment ?a seg_tww4_0_50))
+                 (at start (not_occupied seg_tww3_0_50))
+                 (at start (not_blocked seg_tww3_0_50 airplane_DAEWH))
+
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_tww4_0_50)))
+                 (at end (not_occupied seg_tww4_0_50))
+                 (at end (not (at-segment ?a seg_tww4_0_50)))
+
+                 (at start (occupied seg_tww3_0_50))
+                 (at start (not (not_occupied seg_tww3_0_50)))
+                 (at start (blocked seg_tww3_0_50 ?a))
+                 (at start (not (not_blocked seg_tww3_0_50 ?a)))
+                 (at end (at-segment ?a seg_tww3_0_50))
+
+                 (at end (not (blocked seg_rww_0_50 ?a)))
+                 (at end (not_blocked seg_rww_0_50 ?a))
+                 (at end (not (blocked seg_rwtw1_0_10 ?a)))
+                 (at end (not_blocked seg_rwtw1_0_10 ?a))
+
+                )
+)
+
+(:durative-action move_seg_tww3_0_50_seg_tww2_0_50_north_north_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 1)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a north))
+                 (at start (at-segment ?a seg_tww3_0_50))
+                 (at start (not_occupied seg_tww2_0_50))
+                 (at start (not_blocked seg_tww2_0_50 airplane_DAEWH))
+
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_tww3_0_50)))
+                 (at end (not_occupied seg_tww3_0_50))
+                 (at end (not (at-segment ?a seg_tww3_0_50)))
+
+                 (at start (occupied seg_tww2_0_50))
+                 (at start (not (not_occupied seg_tww2_0_50)))
+                 (at start (blocked seg_tww2_0_50 ?a))
+                 (at start (not (not_blocked seg_tww2_0_50 ?a)))
+                 (at end (at-segment ?a seg_tww2_0_50))
+
+                 (at end (not (blocked seg_tww4_0_50 ?a)))
+                 (at end (not_blocked seg_tww4_0_50 ?a))
+
+                )
+)
+
+(:durative-action move_seg_tww2_0_50_seg_tww1_0_200_north_north_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 1)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a north))
+                 (at start (at-segment ?a seg_tww2_0_50))
+                 (at start (not_occupied seg_tww1_0_200))
+                 (at start (not_blocked seg_tww1_0_200 airplane_DAEWH))
+
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_tww2_0_50)))
+                 (at end (not_occupied seg_tww2_0_50))
+                 (at end (not (at-segment ?a seg_tww2_0_50)))
+
+                 (at start (occupied seg_tww1_0_200))
+                 (at start (not (not_occupied seg_tww1_0_200)))
+                 (at start (blocked seg_tww1_0_200 ?a))
+                 (at start (not (not_blocked seg_tww1_0_200 ?a)))
+                 (at end (at-segment ?a seg_tww1_0_200))
+
+                 (at end (not (blocked seg_tww3_0_50 ?a)))
+                 (at end (not_blocked seg_tww3_0_50 ?a))
+
+                )
+)
+
+(:durative-action move_seg_tww1_0_200_seg_ppdoor_0_40_north_south_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 6)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a north))
+                 (at start (at-segment ?a seg_tww1_0_200))
+                 (at start (not_occupied seg_ppdoor_0_40))
+                 (at start (not_blocked seg_ppdoor_0_40 airplane_DAEWH))
+
+                 (over all (not_occupied seg_twe1_0_200))
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_tww1_0_200)))
+                 (at end (not_occupied seg_tww1_0_200))
+                 (at end (not (at-segment ?a seg_tww1_0_200)))
+
+                 (at start (occupied seg_ppdoor_0_40))
+                 (at start (not (not_occupied seg_ppdoor_0_40)))
+                 (at start (blocked seg_ppdoor_0_40 ?a))
+                 (at start (not (not_blocked seg_ppdoor_0_40 ?a)))
+                 (at end (at-segment ?a seg_ppdoor_0_40))
+                 (at end (not (facing ?a north)))
+                 (at end (facing ?a south))
+
+                 (at end (not (blocked seg_tww2_0_50 ?a)))
+                 (at end (not_blocked seg_tww2_0_50 ?a))
+
+                 (at end (blocked seg_twe1_0_200 ?a))
+                 (at end (not (not_blocked seg_twe1_0_200 ?a)))
+                )
+)
+
+(:durative-action move_seg_ppdoor_0_40_seg_pp_0_60_south_south_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the speed of an airplane (fixed 30 m/s)
+             (= ?duration 1)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-moving ?a))
+                 (at start (facing ?a south))
+                 (at start (at-segment ?a seg_ppdoor_0_40))
+                 (at start (not_occupied seg_pp_0_60))
+                 (at start (not_blocked seg_pp_0_60 airplane_DAEWH))
+
+              )
+
+ :effect        (and
+                 (at end (not (occupied seg_ppdoor_0_40)))
+                 (at end (not_occupied seg_ppdoor_0_40))
+                 (at end (not (at-segment ?a seg_ppdoor_0_40)))
+
+                 (at start (occupied seg_pp_0_60))
+                 (at start (not (not_occupied seg_pp_0_60)))
+                 (at start (blocked seg_pp_0_60 ?a))
+                 (at start (not (not_blocked seg_pp_0_60 ?a)))
+                 (at end (at-segment ?a seg_pp_0_60))
+
+                 (at end (not (blocked seg_tww1_0_200 ?a)))
+                 (at end (not_blocked seg_tww1_0_200 ?a))
+                 (at end (not (blocked seg_twe1_0_200 ?a)))
+                 (at end (not_blocked seg_twe1_0_200 ?a))
+
+                )
+)
+
+(:durative-action pushback_seg_pp_0_60_seg_ppdoor_0_40_south_south_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the pushbackspeed of an airplane (fixed 5 m/s)
+             (= ?duration 12)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-pushing ?a))
+                 (at start (facing ?a south))
+                 (at start (at-segment ?a seg_pp_0_60))
+                 (at start (not_occupied seg_ppdoor_0_40))
+                 (at start (not_blocked seg_ppdoor_0_40 airplane_DAEWH))
+                )
+
+ :effect        (and
+                 (at end (not (occupied seg_pp_0_60)))
+                 (at end (not_occupied seg_pp_0_60))
+                 (at end (not (blocked seg_pp_0_60 ?a)))
+                 (at end (not_blocked seg_pp_0_60 ?a))
+                 (at end (not (at-segment ?a seg_pp_0_60)))
+
+                 (at start (occupied seg_ppdoor_0_40))
+                 (at start (not (not_occupied seg_ppdoor_0_40)))
+                 (at start (blocked seg_ppdoor_0_40 ?a))
+                 (at start (not (not_blocked seg_ppdoor_0_40 ?a)))
+                 (at end (at-segment ?a seg_ppdoor_0_40))
+
+                )
+)
+
+(:durative-action pushback_seg_ppdoor_0_40_seg_tww1_0_200_south_north_medium
+
+ :parameters    (?a - airplane)
+
+ :duration
+             ;; length of the segment divided through the pushbackspeed of an airplane (fixed 5 m/s)
+             (= ?duration 8)
+
+ :condition  (and
+                 (over all (has-type ?a medium))
+                 (over all (is-pushing ?a))
+                 (at start (facing ?a south))
+                 (at start (at-segment ?a seg_ppdoor_0_40))
+                 (at start (not_occupied seg_tww1_0_200))
+                 (at start (not_blocked seg_tww1_0_200 airplane_DAEWH))
+                )
+
+ :effect        (and
+                 (at end (not (occupied seg_ppdoor_0_40)))
+                 (at end (not_occupied seg_ppdoor_0_40))
+                 (at end (not (blocked seg_ppdoor_0_40 ?a)))
+                 (at end (not_blocked seg_ppdoor_0_40 ?a))
+                 (at end (not (at-segment ?a seg_ppdoor_0_40)))
+
+                 (at start (occupied seg_tww1_0_200))
+                 (at start (not (not_occupied seg_tww1_0_200)))
+                 (at start (blocked seg_tww1_0_200 ?a))
+                 (at start (not (not_blocked seg_tww1_0_200 ?a)))
+                 (at end (at-segment ?a seg_tww1_0_200))
+                 (at end (not (facing ?a south)))
+                 (at end (facing ?a north))
+
+                )
+)
+
+
+(:durative-action takeoff_seg_rww_0_50_north
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration 30 )
+
+ :condition    (and (at start (at-segment ?a seg_rww_0_50))
+                    (at start (facing ?a north))
+                    (at start (is-moving ?a))
+               )
+
+ :effect (and (at end (not (occupied seg_rww_0_50)))
+              (at end (not_occupied seg_rww_0_50))
+              (at end (not (blocked seg_rww_0_50 ?a)))
+              (at end (not_blocked seg_rww_0_50 ?a))
+              (at end (not (at-segment ?a seg_rww_0_50)))
+              (at end (airborne ?a seg_rww_0_50))
+              (at end (not (is-moving ?a )))
+              (at end (not (blocked seg_tww4_0_50 ?a)))
+              (at end (not_blocked seg_tww4_0_50 ?a))
+         )
+)
+
+(:durative-action takeoff_seg_rwe_0_50_south
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration 30 )
+
+ :condition    (and (at start (at-segment ?a seg_rwe_0_50))
+                    (at start (facing ?a south))
+                    (at start (is-moving ?a))
+               )
+
+ :effect (and (at end (not (occupied seg_rwe_0_50)))
+              (at end (not_occupied seg_rwe_0_50))
+              (at end (not (blocked seg_rwe_0_50 ?a)))
+              (at end (not_blocked seg_rwe_0_50 ?a))
+              (at end (not (at-segment ?a seg_rwe_0_50)))
+              (at end (airborne ?a seg_rwe_0_50))
+              (at end (not (is-moving ?a )))
+              (at end (not (blocked seg_twe4_0_50 ?a)))
+              (at end (not_blocked seg_twe4_0_50 ?a))
+         )
+)
+
+
+(:durative-action park_seg_pp_0_60_north
+ :parameters (?a - airplane)
+ :duration (= ?duration 40)
+ :condition (and    (over all (at-segment ?a seg_pp_0_60))
+                    (over all (facing ?a north))
+                    (at start (is-moving ?a))
+            )
+ :effect (and (at end (is-parked ?a seg_pp_0_60))
+              (at end (not (is-moving ?a)))
+         )
+)
+(:durative-action park_seg_pp_0_60_south
+ :parameters (?a - airplane)
+ :duration (= ?duration 40)
+ :condition (and    (over all (at-segment ?a seg_pp_0_60))
+                    (over all (facing ?a south))
+                    (at start (is-moving ?a))
+            )
+ :effect (and (at end (is-parked ?a seg_pp_0_60))
+              (at end (not (is-moving ?a)))
+              (at end (not (blocked seg_ppdoor_0_40 ?a)))
+              (at end (not_blocked seg_ppdoor_0_40 ?a))
+         )
+)
+(:durative-action startup_seg_pp_0_60_north_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_pp_0_60))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+         )
+)
+(:durative-action startup_seg_pp_0_60_south_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_pp_0_60))
+                    (over all (not_occupied seg_ppdoor_0_40))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_ppdoor_0_40 ?a))
+                    (at end (not (not_blocked seg_ppdoor_0_40 ?a)))
+         )
+)
+(:durative-action startup_seg_ppdoor_0_40_north_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_ppdoor_0_40))
+                    (over all (not_occupied seg_pp_0_60))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_pp_0_60 ?a))
+                    (at end (not (not_blocked seg_pp_0_60 ?a)))
+         )
+)
+(:durative-action startup_seg_ppdoor_0_40_south_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_ppdoor_0_40))
+                    (over all (not_occupied seg_tww1_0_200))
+                    (over all (not_occupied seg_twe1_0_200))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_tww1_0_200 ?a))
+                    (at end (not (not_blocked seg_tww1_0_200 ?a)))
+                    (at end (blocked seg_twe1_0_200 ?a))
+                    (at end (not (not_blocked seg_twe1_0_200 ?a)))
+         )
+)
+(:durative-action startup_seg_tww1_0_200_north_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_tww1_0_200))
+                    (over all (not_occupied seg_tww2_0_50))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_tww2_0_50 ?a))
+                    (at end (not (not_blocked seg_tww2_0_50 ?a)))
+         )
+)
+(:durative-action startup_seg_tww1_0_200_south_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_tww1_0_200))
+                    (over all (not_occupied seg_ppdoor_0_40))
+                    (over all (not_occupied seg_twe1_0_200))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_ppdoor_0_40 ?a))
+                    (at end (not (not_blocked seg_ppdoor_0_40 ?a)))
+                    (at end (blocked seg_twe1_0_200 ?a))
+                    (at end (not (not_blocked seg_twe1_0_200 ?a)))
+         )
+)
+(:durative-action startup_seg_twe1_0_200_north_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_twe1_0_200))
+                    (over all (not_occupied seg_twe2_0_50))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_twe2_0_50 ?a))
+                    (at end (not (not_blocked seg_twe2_0_50 ?a)))
+         )
+)
+(:durative-action startup_seg_twe1_0_200_south_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_twe1_0_200))
+                    (over all (not_occupied seg_ppdoor_0_40))
+                    (over all (not_occupied seg_tww1_0_200))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_ppdoor_0_40 ?a))
+                    (at end (not (not_blocked seg_ppdoor_0_40 ?a)))
+                    (at end (blocked seg_tww1_0_200 ?a))
+                    (at end (not (not_blocked seg_tww1_0_200 ?a)))
+         )
+)
+(:durative-action startup_seg_tww2_0_50_north_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_tww2_0_50))
+                    (over all (not_occupied seg_tww3_0_50))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_tww3_0_50 ?a))
+                    (at end (not (not_blocked seg_tww3_0_50 ?a)))
+         )
+)
+(:durative-action startup_seg_tww2_0_50_south_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_tww2_0_50))
+                    (over all (not_occupied seg_tww1_0_200))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_tww1_0_200 ?a))
+                    (at end (not (not_blocked seg_tww1_0_200 ?a)))
+         )
+)
+(:durative-action startup_seg_tww3_0_50_north_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_tww3_0_50))
+                    (over all (not_occupied seg_tww4_0_50))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_tww4_0_50 ?a))
+                    (at end (not (not_blocked seg_tww4_0_50 ?a)))
+         )
+)
+(:durative-action startup_seg_tww3_0_50_south_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_tww3_0_50))
+                    (over all (not_occupied seg_tww2_0_50))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_tww2_0_50 ?a))
+                    (at end (not (not_blocked seg_tww2_0_50 ?a)))
+         )
+)
+(:durative-action startup_seg_tww4_0_50_north_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_tww4_0_50))
+                    (over all (not_occupied seg_rww_0_50))
+                    (over all (not_occupied seg_rwtw1_0_10))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_rww_0_50 ?a))
+                    (at end (not (not_blocked seg_rww_0_50 ?a)))
+                    (at end (blocked seg_rwtw1_0_10 ?a))
+                    (at end (not (not_blocked seg_rwtw1_0_10 ?a)))
+         )
+)
+(:durative-action startup_seg_tww4_0_50_south_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_tww4_0_50))
+                    (over all (not_occupied seg_tww3_0_50))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_tww3_0_50 ?a))
+                    (at end (not (not_blocked seg_tww3_0_50 ?a)))
+         )
+)
+(:durative-action startup_seg_twe4_0_50_north_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_twe4_0_50))
+                    (over all (not_occupied seg_rwte1_0_10))
+                    (over all (not_occupied seg_rwe_0_50))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_rwte1_0_10 ?a))
+                    (at end (not (not_blocked seg_rwte1_0_10 ?a)))
+                    (at end (blocked seg_rwe_0_50 ?a))
+                    (at end (not (not_blocked seg_rwe_0_50 ?a)))
+         )
+)
+(:durative-action startup_seg_twe4_0_50_south_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_twe4_0_50))
+                    (over all (not_occupied seg_twe3_0_50))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_twe3_0_50 ?a))
+                    (at end (not (not_blocked seg_twe3_0_50 ?a)))
+         )
+)
+(:durative-action startup_seg_twe3_0_50_north_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_twe3_0_50))
+                    (over all (not_occupied seg_twe4_0_50))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_twe4_0_50 ?a))
+                    (at end (not (not_blocked seg_twe4_0_50 ?a)))
+         )
+)
+(:durative-action startup_seg_twe3_0_50_south_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_twe3_0_50))
+                    (over all (not_occupied seg_twe2_0_50))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_twe2_0_50 ?a))
+                    (at end (not (not_blocked seg_twe2_0_50 ?a)))
+         )
+)
+(:durative-action startup_seg_twe2_0_50_north_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_twe2_0_50))
+                    (over all (not_occupied seg_twe3_0_50))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_twe3_0_50 ?a))
+                    (at end (not (not_blocked seg_twe3_0_50 ?a)))
+         )
+)
+(:durative-action startup_seg_twe2_0_50_south_medium
+
+ :parameters (?a - airplane)
+
+ :duration  (= ?duration (* 60 (engines ?a) ) )
+
+ :condition    (and (at start (is-pushing ?a))
+                    (over all (at-segment ?a seg_twe2_0_50))
+                    (over all (not_occupied seg_twe1_0_200))
+               )
+
+ :effect       (and (at end (not (is-pushing ?a)))
+                    (at end (is-moving ?a))
+                    (at end (blocked seg_twe1_0_200 ?a))
+                    (at end (not (not_blocked seg_twe1_0_200 ?a)))
+         )
+)
+)
diff --git a/tests/data/pddl/temporal/ipc18/airport-temporal-strips/2/instance-2.pddl b/tests/data/pddl/temporal/ipc18/airport-temporal-strips/2/instance-2.pddl
new file mode 100644
index 00000000..bf00684f
--- /dev/null
+++ b/tests/data/pddl/temporal/ipc18/airport-temporal-strips/2/instance-2.pddl
@@ -0,0 +1,100 @@
+;;
+;; PDDL file for the AIPS2000 Planning Competition
+;; based on the data generated by the airport simulator Astras.
+;;
+
+;; Author: Sebastian Trueg thisshouldbethecurrentdateandtime :(
+;; Created with PreInstancerAirportExporter 0.5 by Sebastian Trueg <trueg@informatik.uni-freiburg.de>
+;;
+
+
+
+(define (problem PROBLEM_2)
+
+(:domain airport_fixed_structure)
+
+(:objects
+)
+
+(:init
+
+      (at-segment airplane_DAEWH seg_pp_0_60)
+
+      (blocked seg_pp_0_60 airplane_DAEWH)
+
+      (facing airplane_DAEWH south)
+
+      (has-type airplane_DAEWH medium)
+
+
+      (is-pushing airplane_DAEWH)
+
+
+      (not_blocked seg_ppdoor_0_40 airplane_DAEWH)
+      (not_blocked seg_tww1_0_200 airplane_DAEWH)
+      (not_blocked seg_twe1_0_200 airplane_DAEWH)
+      (not_blocked seg_tww2_0_50 airplane_DAEWH)
+      (not_blocked seg_tww3_0_50 airplane_DAEWH)
+      (not_blocked seg_tww4_0_50 airplane_DAEWH)
+      (not_blocked seg_rww_0_50 airplane_DAEWH)
+      (not_blocked seg_rwtw1_0_10 airplane_DAEWH)
+      (not_blocked seg_rw_0_400 airplane_DAEWH)
+      (not_blocked seg_rwe_0_50 airplane_DAEWH)
+      (not_blocked seg_twe4_0_50 airplane_DAEWH)
+      (not_blocked seg_rwte1_0_10 airplane_DAEWH)
+      (not_blocked seg_twe3_0_50 airplane_DAEWH)
+      (not_blocked seg_twe2_0_50 airplane_DAEWH)
+      (not_blocked seg_rwte2_0_10 airplane_DAEWH)
+      (not_blocked seg_rwtw2_0_10 airplane_DAEWH)
+
+      (not_occupied seg_ppdoor_0_40)
+      (not_occupied seg_tww1_0_200)
+      (not_occupied seg_twe1_0_200)
+      (not_occupied seg_tww2_0_50)
+      (not_occupied seg_tww3_0_50)
+      (not_occupied seg_tww4_0_50)
+      (not_occupied seg_rww_0_50)
+      (not_occupied seg_rwtw1_0_10)
+      (not_occupied seg_rw_0_400)
+      (not_occupied seg_rwe_0_50)
+      (not_occupied seg_twe4_0_50)
+      (not_occupied seg_rwte1_0_10)
+      (not_occupied seg_twe3_0_50)
+      (not_occupied seg_twe2_0_50)
+      (not_occupied seg_rwte2_0_10)
+      (not_occupied seg_rwtw2_0_10)
+
+      (occupied seg_pp_0_60)
+      (= (length seg_pp_0_60) 60)
+      (= (length seg_ppdoor_0_40) 40)
+      (= (length seg_tww1_0_200) 200)
+      (= (length seg_twe1_0_200) 200)
+      (= (length seg_tww2_0_50) 50)
+      (= (length seg_tww3_0_50) 50)
+      (= (length seg_tww4_0_50) 50)
+      (= (length seg_rww_0_50) 50)
+      (= (length seg_rwtw1_0_10) 10)
+      (= (length seg_rw_0_400) 400)
+      (= (length seg_rwe_0_50) 50)
+      (= (length seg_twe4_0_50) 50)
+      (= (length seg_rwte1_0_10) 10)
+      (= (length seg_twe3_0_50) 50)
+      (= (length seg_twe2_0_50) 50)
+      (= (length seg_rwte2_0_10) 10)
+      (= (length seg_rwtw2_0_10) 10)
+      (= (engines airplane_DAEWH) 2)
+)
+
+(:goal
+      (and
+
+
+
+            (airborne airplane_DAEWH seg_rwe_0_50)
+      )
+)
+
+(:metric minimize (total-time))
+
+)
+
diff --git a/tests/data/pddl/temporal/ipc18/cushing/domain.pddl b/tests/data/pddl/temporal/ipc18/cushing/domain.pddl
new file mode 100644
index 00000000..25778151
--- /dev/null
+++ b/tests/data/pddl/temporal/ipc18/cushing/domain.pddl
@@ -0,0 +1,51 @@
+(define (domain testdomain)
+	(:requirements :equality :typing :durative-actions)
+	(:types variable)
+	(:predicates
+		(norepeat ?var - variable)
+		(condition1 ?var - variable)
+		(condition2 ?var - variable)
+		(condition3 ?var - variable)
+		(target1 ?var - variable)
+		(target2 ?var - variable)
+		(target3 ?var - variable)
+	)
+
+	(:durative-action action_type1
+		:parameters (?var - variable)
+		:duration (= ?duration 5)
+		:condition (and
+			(at start (norepeat ?var))
+		)
+		:effect (and
+			(at start (condition1 ?var))
+			(at start (not (norepeat ?var)))
+			(at end (not (condition1 ?var)))
+			(at end (target1 ?var))
+			(at end (not (target2 ?var)))
+		)
+	)
+	(:durative-action action_type2
+		:parameters (?var - variable)
+		:duration (= ?duration 4)
+		:condition (and
+			(at start (condition1 ?var))
+		)
+		:effect (and
+			(at start (condition2 ?var))
+			(at end (not (condition2 ?var)))
+			(at end (target2 ?var))
+		)
+	)
+	(:durative-action action_type3
+		:parameters (?var - variable)
+		:duration (= ?duration 1)
+		:condition (and
+			(at start (condition2 ?var))
+		)
+		:effect (and
+			(at end (not (target1 ?var)))
+			(at end (target3 ?var))
+		)
+	)
+)
diff --git a/tests/data/pddl/temporal/ipc18/cushing/pfile1.pddl b/tests/data/pddl/temporal/ipc18/cushing/pfile1.pddl
new file mode 100644
index 00000000..d991f5d2
--- /dev/null
+++ b/tests/data/pddl/temporal/ipc18/cushing/pfile1.pddl
@@ -0,0 +1,21 @@
+(define (problem pfile1)
+	(:domain testdomain)
+	(:objects 
+		var1 var2 - variable
+	)
+	(:init
+		(norepeat var1)
+		(norepeat var2)
+	)
+	(:goal
+		(and
+			(target1 var1)
+			(target2 var1)
+			(target3 var1)
+			(target1 var2)
+			(target2 var2)
+			(target3 var2)
+		)
+	)
+	(:metric minimize (total-time))
+)
diff --git a/tests/fol/test_syntax.py b/tests/fol/test_syntax.py
index c28987f3..0601ae0d 100755
--- a/tests/fol/test_syntax.py
+++ b/tests/fol/test_syntax.py
@@ -383,3 +383,15 @@ def test_language_equality():
     #
     # lang1.constant('c', 'object')
     # assert lang1 != lang2
+
+
+def test_equality_and_terms_behaviour():
+    lang = fstrips.language(theories=[Theory.EQUALITY, Theory.ARITHMETIC])
+
+    f = lang.function('foo', lang.Object, lang.Object)
+    x = lang.variable('?x', lang.Object)
+    o = lang.constant('o', lang.Object)
+
+    phi = f(o) == x
+
+    assert isinstance(phi, Atom)
\ No newline at end of file
diff --git a/tests/fstrips/test_representation.py b/tests/fstrips/test_representation.py
index cc2977db..bbfeaabd 100644
--- a/tests/fstrips/test_representation.py
+++ b/tests/fstrips/test_representation.py
@@ -183,7 +183,7 @@ def test_neg_precondition_compilation_on_formulas():
     x = lang.variable('x', 'object')
     y = lang.variable('y', 'object')
 
-    negpreds = dict()
+    negpreds = {}
 
     comp = compile_away_formula_negated_literals(clear(x) & (x != y), negpreds)
     assert comp == clear(x) & (x != y) and not negpreds  # No change was made
@@ -208,7 +208,7 @@ def test_neg_precondition_compilation_on_action():
     clear, on, ontable, handempty, holding = lang.get('clear', 'on', 'ontable', 'handempty', 'holding')
     x = lang.variable('x', 'object')
 
-    negpreds = dict()
+    negpreds = {}
     
     pickup = problem.get_action('pick-up')
     pickupc = compile_action_negated_preconditions_away(pickup, negpreds)
diff --git a/tests/fstrips/test_symbol_classification.py b/tests/fstrips/test_symbol_classification.py
index a20edc25..cd8a42eb 100644
--- a/tests/fstrips/test_symbol_classification.py
+++ b/tests/fstrips/test_symbol_classification.py
@@ -1,3 +1,4 @@
+import pytest
 from tarski.grounding.ops import approximate_symbol_fluency
 from tarski.syntax.util import get_symbols
 
@@ -19,7 +20,7 @@
 ]
 
 SAMPLE_FSTRIPS_INSTANCES = [
-    "counters-fn:instance_4.pddl",  # functions, ints
+   "counters-fn:instance_4.pddl",  # functions, ints
 ]
 
 
diff --git a/tests/io/test_fd_sas_writer.py b/tests/io/test_fd_sas_writer.py
new file mode 100644
index 00000000..d246030c
--- /dev/null
+++ b/tests/io/test_fd_sas_writer.py
@@ -0,0 +1,189 @@
+import pytest
+
+import tempfile
+
+import tarski as tsk
+from tarski.theories import Theory
+from tarski.syntax import land, symref
+from tarski.io.sas.fd import Writer
+from tarski.sas import Action
+from tarski.util import SymbolIndex
+
+@pytest.mark.sas
+def test_gripper_instance():
+    """
+    Follows the example given in:
+
+    https://www.fast-downward.org/TranslatorOutputFormat4
+    """
+
+    # Gripper domain theory
+    L = tsk.language("gripper", theories=[Theory.BOOLEAN, Theory.EQUALITY, Theory.ARITHMETIC])
+
+    # Types
+    ball_t = L.sort('ball', L.Object)
+    gripper_t = L.sort('gripper', L.Object)
+    room_t = L.sort('room', L.Object)
+
+    # Constant objects
+    none = L.constant("none", L.Object)
+    the_balls = [L.constant('ball{}'.format(i), ball_t) for i in range(4)]
+    grippers = [L.constant(gripper_name, gripper_t) for gripper_name in ('left', 'right')]
+    the_rooms = [L.constant('room{}'.format(i), room_t) for i in ('a', 'b')]
+
+    # predicates
+    carry = L.function('carry', ball_t, gripper_t, L.Integer)
+    free = L.function('free', gripper_t, L.Integer)
+    at_ball = L.function('at_ball', ball_t, room_t, L.Integer)
+    at_robot = L.function('at_robot', room_t, L.Integer)
+
+    # SAS instance data structures
+    # variable set
+    X = SymbolIndex()
+    # domains
+    D = {}
+
+    # Theory that results from Helmert's invariant detection algorithm
+
+    # Constants
+    L2 = tsk.language("mv-gripper", theories=[Theory.EQUALITY])
+    none = L2.constant("<none of those>", L2.Object)
+
+    var0 = L2.function("var0", L2.Object)
+    var0_domain = [
+        L2.constant(str(carry(the_balls[0], grippers[1])), L2.Object),
+        L2.constant(str(carry(the_balls[1], grippers[1])), L2.Object),
+        L2.constant(str(carry(the_balls[2], grippers[1])), L2.Object),
+        L2.constant(str(free(grippers[1])), L2.Object),
+        L2.constant(str(carry(the_balls[3], grippers[1])), L2.Object)
+    ]
+
+    X.add(symref(var0()))
+    D[symref(var0())] = SymbolIndex()
+    for v in var0_domain:
+        D[symref(var0())].add(symref(v))
+
+    var1 = L2.function("var1", L2.Object)
+    var1_domain = [
+        L2.constant(str(carry(the_balls[2], grippers[0])), L2.Object),
+        L2.constant(str(free(grippers[0])), L2.Object),
+        L2.constant(str(carry(the_balls[1], grippers[0])), L2.Object),
+        L2.constant(str(carry(the_balls[0], grippers[0])), L2.Object),
+        L2.constant(str(carry(the_balls[3], grippers[0])), L2.Object)
+    ]
+
+    X.add(symref(var1()))
+    D[symref(var1())] = SymbolIndex()
+    for v in var1_domain:
+        D[symref(var1())].add(symref(v))
+
+    var2 = L2.function("var2", L2.Object)
+    var2_domain = [
+        L2.constant(str(at_ball(the_balls[3], the_rooms[0])), L2.Object),
+        L2.constant(str(at_ball(the_balls[3], the_rooms[1])), L2.Object),
+        none
+    ]
+
+    X.add(symref(var2()))
+    D[symref(var2())] = SymbolIndex()
+    for v in var2_domain:
+        D[symref(var2())].add(symref(v))
+
+    var3 = L2.function("var3", L2.Object)
+    var3_domain = [
+        L2.constant(str(at_ball(the_balls[2], the_rooms[0])), L2.Object),
+        L2.constant(str(at_ball(the_balls[2], the_rooms[1])), L2.Object),
+        none
+    ]
+
+    X.add(symref(var3()))
+    D[symref(var3())] = SymbolIndex()
+    for v in var3_domain:
+        D[symref(var3())].add(symref(v))
+
+    var4 = L2.function("var4", L2.Object)
+    var4_domain = [
+        L2.constant(str(at_ball(the_balls[0], the_rooms[0])), L2.Object),
+        L2.constant(str(at_ball(the_balls[0], the_rooms[1])), L2.Object),
+        none
+    ]
+
+    X.add(symref(var4()))
+    D[symref(var4())] = SymbolIndex()
+    for v in var4_domain:
+        D[symref(var4())].add(symref(v))
+
+    var5 = L2.function("var5", L2.Object)
+    var5_domain = [
+        L2.constant(str(at_ball(the_balls[1], the_rooms[0])), L2.Object),
+        L2.constant(str(at_ball(the_balls[1], the_rooms[1])), L2.Object),
+        none
+    ]
+
+    X.add(symref(var5()))
+    D[symref(var5())] = SymbolIndex()
+    for v in var5_domain:
+        D[symref(var5())].add(symref(v))
+
+    var6 = L2.function("var6", L2.Object)
+    var6_domain = [
+        L2.constant(str(at_robot(the_rooms[1])), L2.Object),
+        L2.constant(str(at_robot(the_rooms[0])), L2.Object)
+    ]
+
+    X.add(symref(var6()))
+    D[symref(var6())] = SymbolIndex()
+    for v in var6_domain:
+        D[symref(var6())].add(symref(v))
+
+    # actions
+    # MRJ: Note that here we're divergin from the FD docs, as the given preconditions do not seem correct. Note that
+    # below we have what in my opinion is a proper SAS formulation of gripper, where 1) the robot must remain at the
+    # room while dropping the ball on the ground, 2) the multi-valued variable for the location of ball3 (roomA, roomB,
+    # <none of those>) requires, for each transition (s,a,s') that s satisfies var3 = <none of these>. Otherwise it
+    # would be possible (in principle) to allow transitions where var3 has an inconvenient value (like the ball being in
+    # different room).
+    # Otherwise, states then are to be interpreted as a CSP with equality constraints, and "mutex groups", constraints
+    # of the form x=v -> x'\neq v' (or x \neq v \lor x' \neq v'). I haven't heard of states as CSPs (with multiple models?)
+    # How would this work?
+    Act = [
+        Action(name='drop',
+               arguments=[the_balls[0], the_rooms[0], grippers[0]],
+               transitions=[
+                   (var1(), var1_domain[3], var1_domain[1]),
+                   (var6(), var6_domain[0], var6_domain[0]),
+                   (var3(), var3_domain[2], var3_domain[0])
+               ])
+    ]
+
+
+    # initial state
+    # note that here we diverge from the example in https://www.fast-downward.org/TranslatorOutputFormat4 which
+    # does not give sensible values for the state variables given
+    initial_state = [
+        (var0(), var0_domain[0]), # robot carries ball1 on right gripper
+        (var1(), var1_domain[0]), # robot carries ball3 on left gripper
+        (var2(), var2_domain[0]), # ball 4 is at room a
+        (var3(), var3_domain[2]), # ball 3 is <none of those>
+        (var4(), var4_domain[2]), # ball 1 is <none of those>
+        (var5(), var5_domain[0]), # ball 2 is at room a
+        (var6(), var6_domain[0]), # robot is at room b
+    ]
+
+    goal_state = [
+        (var2(), var2_domain[1]), # ball 4 is at room b
+        (var3(), var3_domain[1]), # ball 3 is at room b
+        (var4(), var4_domain[1]), # ball 1 is at room b
+        (var5(), var5_domain[1]), # ball 2 is at room b
+    ]
+
+    with tempfile.TemporaryFile(mode='w+') as outstream:
+        Writer(theory=L2,
+               state_variables=X,
+               domains=D,
+               actions=Act,
+               initial=initial_state,
+               goal=goal_state).dump(outstream)
+        outstream.seek(0)
+        written_data = outstream.read()
+        print(written_data)
\ No newline at end of file
diff --git a/tests/io/test_fstrips_parsing.py b/tests/io/test_fstrips_parsing.py
index 172da94a..5484b346 100644
--- a/tests/io/test_fstrips_parsing.py
+++ b/tests/io/test_fstrips_parsing.py
@@ -320,4 +320,4 @@ def test_increase_effects():
 
     increase = output[1][0]
     assert isinstance(increase, FunctionalEffect) and isinstance(increase.condition, Tautology)
-    assert str(increase.rhs) == '+(total-cost(), 1)'
+    assert str(increase.rhs) == '+(total-cost(), 1.0)'
diff --git a/tests/io/test_pddl_parsing.py b/tests/io/test_pddl_parsing.py
new file mode 100644
index 00000000..5f0a4f0d
--- /dev/null
+++ b/tests/io/test_pddl_parsing.py
@@ -0,0 +1,224 @@
+# Unit tests for PDDL lexer, parser and instance structure
+#
+# Contact: Miquel Ramirez (miquel.ramirez@pm.me)
+import tempfile
+
+import pytest
+from tarski.io.pddl.lexer import PDDLlex
+from tarski.io.pddl import Features
+from tarski.io.pddl.parser import PDDLparser, UnsupportedFeature
+from tarski.syntax.visitors import CollectEqualityAtoms
+
+
+@pytest.mark.pddl
+def test_pddl_lexer_airport_002_domain():
+
+    pddl_data = None
+    with open('tests/data/pddl/temporal/ipc18/airport-temporal-strips/2/domain.pddl') as instream:
+        pddl_data = instream.read()
+
+    lexer = PDDLlex()
+    lexer.build()
+
+    lexer.input(pddl_data)
+
+    count = 0
+    for tok in lexer():
+        count += 1
+
+    assert count == 5930
+
+
+@pytest.mark.pddl
+def test_pddl_lexer_cushing_domain():
+
+    pddl_data = None
+    with open('tests/data/pddl/temporal/ipc18/cushing/domain.pddl') as instream:
+        pddl_data = instream.read()
+
+    lexer = PDDLlex()
+    lexer.build()
+
+    lexer.input(pddl_data)
+
+    count = 0
+    for tok in lexer():
+        count += 1
+
+    assert count == 253
+
+
+@pytest.mark.pddl
+def test_basic_constructs():
+    pddl_data = """(define 
+        (domain FOO)
+        (:requirements :durative-actions :derived-predicates :typing)
+        (:types 
+                paz naz - caz
+                bar baz
+        )
+        (:constants
+            a0 a1 a2 a3 a4 a5 - bar
+            d0 d1 d2 d3 d4 d5 - baz
+            b1 b2 b3 b4 b5 b6 - paz
+            c1 c2 c3 c4 c5 c6 - naz
+        )
+        (:predicates 
+            (p ?x - bar ?y - baz)
+            (q ?x - bar ?y - baz)
+            (r ?x - bar)
+            (s ?x - bar)
+            (t1 ?y - bar)
+            (t2 ?y - bar)
+            (t3 ?y - bar)
+            (t4 ?y - bar)
+        )
+
+        (:functions
+            (h ?x - bar) - baz
+            (g ?x - baz)
+        )
+
+        (:action dummy1
+            :parameters (?x - bar)
+            :precondition   (and
+                                    (not (r ?x)) 
+                            )
+            :effect     (and
+                            (r ?x)
+                        )
+        )
+
+        (:durative-action dummy
+            :parameters (?x - bar ?y - baz)
+            :duration   (= ?duration 10)
+            :condition  (and
+                            (at start (q ?x ?y))
+                            (over all (r ?x)) 
+                        )
+            :effect     (and
+                            (at start (q ?x ?y))
+                            (at end (not (q ?x ?y)))
+                        )
+        )
+
+        (:derived
+            (s ?x - bar)
+            (and    (t1 ?x)
+                    (t2 ?x)
+                    (t3 ?x)
+                    (t4 ?x)
+            )
+        )
+    )
+
+    (define (problem INSTANCE_001)
+        (:domain FOO)
+        (:objects
+            z0 z1 z2 z3 - bar
+        )
+        (:init
+            (p z2 d2)
+            (p z3 d3)
+        )
+        (:goal
+            (and    (p z0 d0)
+                    (p z1 d1))
+        )
+        (:metric
+            minimize
+                (total-time)
+        )
+    )
+    """
+    parser = PDDLparser(debug=True)
+
+    with tempfile.NamedTemporaryFile() as f:
+        parser.build(logfile=f.name)
+
+        parser.parse(pddl_data)
+
+        assert parser.domain_name == 'foo'
+        assert parser.problem_name == 'instance_001'
+        assert Features.DURATIVE_ACTIONS in parser.required_features
+        assert Features.TYPING in parser.required_features
+
+        instance = parser.instance
+        assert instance is not None
+
+        print("Functions", len(instance.functions))
+        print("Predicates", len(instance.predicates))
+        print("Types", len(instance.types))
+        print("Constants", len(instance.domains))
+        print("Actions: instantaneous: {} durative: {}".format(len(instance.actions), len(instance.durative)))
+        print("Derived predicates:", len(instance.derived))
+        print("Initial State literals", len(instance.init))
+
+        eq_atoms_visitor = CollectEqualityAtoms()
+        eq_atoms_visitor.visit(instance.goal)
+        goal_atoms = eq_atoms_visitor.atoms
+        print("Goal literals", len(goal_atoms))
+
+        assert len(instance.types) == 6
+        assert 'object' in instance.types
+        assert len(instance.domains) == 4
+        assert len(instance.functions) == 2
+        assert len(instance.predicates) == 8
+        assert len(instance.types) == 6
+        assert len(instance.actions) == 1
+        assert len(instance.durative) == 1
+        assert len(instance.derived) == 1
+        assert len(instance.init) == 2
+        assert len(goal_atoms) == 2
+
+
+@pytest.mark.pddl
+def test_temporal_numeric():
+
+    pddl_data = ""
+
+    with open('tests/data/pddl/temporal/ipc08/elevators/p01-domain.pddl') as instream:
+        pddl_data = instream.read()
+
+    with open('tests/data/pddl/temporal/ipc08/elevators/p01.pddl') as instream:
+        pddl_data += instream.read()
+
+    parser = PDDLparser(debug=True)
+
+    with tempfile.NamedTemporaryFile() as f:
+        parser.build(logfile=f.name)
+
+        parser.parse(pddl_data)
+
+        #assert parser.domain_name == 'foo'
+        #assert parser.problem_name == 'instance_001'
+        #assert Features.DURATIVE_ACTIONS in parser.required_features
+        #assert Features.TYPING in parser.required_features
+
+        instance = parser.instance
+        assert instance is not None
+
+        print("Functions", len(instance.functions))
+        print("Predicates", len(instance.predicates))
+        print("Types", len(instance.types))
+        print("Constants", len(instance.domains))
+        print("Actions: instantaneous: {} durative: {}".format(len(instance.actions), len(instance.durative)))
+        print("Derived predicates:", len(instance.derived))
+        print("Initial State literals", len(instance.init))
+
+        eq_atoms_visitor = CollectEqualityAtoms()
+        eq_atoms_visitor.visit(instance.goal)
+        goal_atoms = eq_atoms_visitor.atoms
+        print("Goal literals", len(goal_atoms))
+
+        #assert len(instance.types) == 6
+        #assert 'object' in instance.types
+        #assert len(instance.constants) == 4
+        #assert len(instance.functions) == 2
+        #assert len(instance.predicates) == 8
+        #assert len(instance.types) == 6
+        #assert len(instance.actions) == 1
+        #assert len(instance.durative) == 1
+        #assert len(instance.derived) == 1
+        #assert len(instance.init) == 2
+        #assert len(goal_atoms) == 2
\ No newline at end of file
diff --git a/tests/issues/__init__.py b/tests/issues/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/issues/test_issue113.py b/tests/issues/test_issue113.py
new file mode 100644
index 00000000..d7597b0f
--- /dev/null
+++ b/tests/issues/test_issue113.py
@@ -0,0 +1,12 @@
+from tarski.io import FstripsWriter
+from tests.io.common import parse_benchmark_instance
+
+
+def test_issue():
+    problem = parse_benchmark_instance("blocks:probBLOCKS-4-0.pddl")
+    writer = FstripsWriter(problem)
+    domain = writer.print_domain()
+    # Simply check that an untyped problem is printed with the default object type
+    assert """(:types
+        object
+    )""" in domain
diff --git a/tests/issues/test_issue114.py b/tests/issues/test_issue114.py
new file mode 100644
index 00000000..b701f43f
--- /dev/null
+++ b/tests/issues/test_issue114.py
@@ -0,0 +1,8 @@
+from tarski.io import PDDLReader
+from tests.data import resolve_path
+
+
+def test_issue():
+    reader = PDDLReader(raise_on_error=True, strict_with_requirements=False)
+    reader.parse_domain(resolve_path('pddl/issue114/domain.pddl'))
+    problem = reader.parse_instance(resolve_path('pddl/issue114/problem.pddl'))
diff --git a/tests/ndl/test_temporal.py b/tests/ndl/test_temporal.py
index 90241186..c081fce5 100644
--- a/tests/ndl/test_temporal.py
+++ b/tests/ndl/test_temporal.py
@@ -8,6 +8,7 @@
 from tarski.ndl import temporal
 from tarski.ndl.temporal import TimedEffect, SetLiteralEffect
 
+
 def test_resource_lock_creation():
 
     with pytest.raises(temporal.NDLSyntaxError):
@@ -36,6 +37,7 @@ def test_resource_lock_creation():
         "ts": 0.0, "td": 10.0, "r": engaged(bearing)
     })
 
+
 def test_resource_level_creation():
 
     L = tsk.language("mylang", theories=[Theory.EQUALITY, Theory.ARITHMETIC])
@@ -49,6 +51,7 @@ def test_resource_level_creation():
         "ts": 0.0, "td": 20.0, "r": direction(rov1), "n": L.constant(0, int_t)
     })
 
+
 def test_action_creation():
 
     L = tsk.language("mylang", theories=[Theory.EQUALITY, Theory.ARITHMETIC])
@@ -88,6 +91,7 @@ def test_action_creation():
     assert len(act.locks) == 1
     assert len(act.levels) == 1
 
+
 @pytest.mark.skip(reason="Instance class and interface still work in progress")
 def test_instance_creation():
 
diff --git a/tests/sas/__init__.py b/tests/sas/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/sas/test_action.py b/tests/sas/test_action.py
new file mode 100644
index 00000000..c7bb7a27
--- /dev/null
+++ b/tests/sas/test_action.py
@@ -0,0 +1,90 @@
+import pytest
+import tarski as tsk
+from tarski.theories import Theory
+from tarski.syntax import land, symref
+from tarski.sas import Schema, Action
+from tarski.sas.temporal import TemporalAction
+from tarski.sas.util import ground_temporal_action
+from itertools import combinations, permutations
+import tarski.model
+from tarski.evaluators.simple import evaluate
+
+
+@pytest.mark.sas
+def test_temporal_action():
+    """
+    Simple test for durative actions, based on the quantum circuit compilation benchmark
+    :return:
+    """
+    L = tsk.language("qcc", theories=[Theory.BOOLEAN, Theory.EQUALITY, Theory.ARITHMETIC])
+
+    # sorts
+    # qbits in a quantum circuit
+    qbits = [L.constant('n{}'.format(i), L.Object) for i in range(4)]
+    # quantum state
+    qstates = [L.constant('q{}'.format(i), L.Object) for i in range(4)]
+    # used for representing quantum states that are in the process of transferring between
+    # two qbits
+    moving = L.constant('moving', L.Object)
+
+    # qstate location
+    location = L.function('location', L.Object, L.Object)
+    # static predicate
+    adj = L.predicate('adjacent', L.Object, L.Object)
+
+    target0 = L.variable('target0', L.Object)
+    target1 = L.variable('target1', L.Object)
+    src = L.variable('src', L.Object)
+    dst = L.variable('dst', L.Object)
+
+    swap_0 = Schema(name='swap_0',
+                    variables=[(target0, qstates), (target1, qstates), (src, qbits), (dst, qbits)],
+                    constraints=[src != dst, target0 != target1, adj(src, dst)],
+                    transitions=[
+                        (location(target0), src, moving),
+                        (location(target1), dst, moving)
+                    ])
+    swap_inv = Schema(name='swap_inv',
+                      variables=[(target0, qstates), (target1, qstates)],
+                      constraints=[target0 != target1],
+                      transitions=[
+                          (location(target0), moving, moving),
+                          (location(target1), moving, moving),
+                      ])
+    swap_f = Schema(name='swap_f',
+                    variables=[(target0, qstates), (target1, qstates), (src, qbits), (dst, qbits)],
+                    constraints=[src != dst, target0 != target1, adj(src, dst)],
+                    transitions=[
+                        (location(target0), moving, dst),
+                        (location(target1), moving, src)
+                    ])
+
+    s = tarski.model.create(L)
+    s.evaluator = evaluate
+
+    # adj constraint
+    for k in range(1, len(qbits)):
+        s.add(adj, qbits[k-1], qbits[k])
+        s.add(adj, qbits[k], qbits[k-1])
+
+    epsilon = 0.001
+    swap_schema = TemporalAction(name='swap', events=[(0.001, swap_0), (2.0, swap_inv), (0.001, swap_f)])
+
+    swap_grounded = ground_temporal_action(L, s, swap_schema)
+    assert len(swap_grounded) == 72
+
+    swap_simple = Schema(name='swap_0',
+                        variables=[(target0, qstates), (target1, qstates), (src, qbits), (dst, qbits)],
+                        constraints=[src != dst, target0 != target1],
+                        transitions=[
+                            (location(target0), src, dst),
+                            (location(target1), dst, src)
+                        ])
+    swap_schema2 = TemporalAction(name='swap', events=[(2.0, swap_simple)])
+
+
+
+
+
+
+