pypddl-datasets 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pypddl_datasets-0.0.1/.gitignore +7 -0
- pypddl_datasets-0.0.1/PKG-INFO +110 -0
- pypddl_datasets-0.0.1/README.md +97 -0
- pypddl_datasets-0.0.1/pyproject.toml +27 -0
- pypddl_datasets-0.0.1/scripts/package_data.py +85 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/__init__.py +311 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/README.md +18 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/__init__.py +1 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/__init__.py +1 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/barman/domain.pddl +170 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/barman/generate_instances.py +97 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/barman/generator.py +145 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/blocks_3/domain.pddl +24 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/blocks_3/generate_instances.py +83 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/blocks_3/generator.py +83 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/blocks_4/domain.pddl +31 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/blocks_4/generate_instances.py +83 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/blocks_4/generator.py +83 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/childsnack/domain.pddl +109 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/childsnack/generate_instances.py +83 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/childsnack/generator.py +96 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/delivery/__init__.py +5 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/delivery/domain.pddl +42 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/delivery/generate_instances.py +83 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/delivery/generator.py +95 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/driverlog/domain.pddl +79 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/driverlog/generate_instances.py +101 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/driverlog/generator.py +189 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/ferry/README.md +14 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/ferry/__init__.py +5 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/ferry/domain.pddl +32 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/ferry/generate_instances.py +83 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/ferry/generator.py +79 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/floortile/__init__.py +5 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/floortile/domain.pddl +82 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/floortile/generate_instances.py +77 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/floortile/generator.py +105 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/goldminer/__init__.py +5 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/goldminer/domain.pddl +97 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/goldminer/generate_instances.py +77 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/goldminer/generator.py +128 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/grid/__init__.py +5 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/grid/domain.pddl +52 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/grid/generate_instances.py +77 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/grid/generator.py +157 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/gripper/README.md +9 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/gripper/__init__.py +5 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/gripper/domain.pddl +36 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/gripper/generate_instances.py +82 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/gripper/generator.py +52 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking/__init__.py +5 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking/domain.pddl +83 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking/generate_instances.py +82 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking/generator.py +92 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking_binary/__init__.py +5 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking_binary/domain.pddl +137 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking_binary/generate_instances.py +77 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking_binary/generator.py +93 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/logistics/__init__.py +5 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/logistics/domain.pddl +83 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/logistics/generate_instances.py +91 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/logistics/generator.py +139 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/miconic/__init__.py +5 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/miconic/domain.pddl +60 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/miconic/generate_instances.py +82 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/miconic/generator.c +954 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/miconic/generator.py +68 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/nomystery/domain.pddl +68 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/nomystery/generator.py +337 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/rovers/domain.pddl +92 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/rovers/generator.py +384 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/satellite/__init__.py +5 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/satellite/domain.pddl +50 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/satellite/generate_instances.py +79 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/satellite/generator.py +111 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/schedule/domain.pddl +202 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/schedule/generator.py +227 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/spanner/domain.pddl +36 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/spanner/generator.py +91 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/tpp/README.md +2 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/tpp/__init__.py +5 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/tpp/domain.pddl +65 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/tpp/generate_instances.py +82 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/tpp/generator.py +106 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/visitall/domain.pddl +40 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/visitall/generator.py +142 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/woodworking/domain.pddl +300 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/woodworking/generator.py +431 -0
- pypddl_datasets-0.0.1/src/pypddl_datasets/suites.py +1023 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pypddl-datasets
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Download PDDL planning benchmark suites: classical, numeric, profiling, and learning splits.
|
|
5
|
+
Project-URL: Homepage, https://github.com/planning-and-learning/planning-benchmarks
|
|
6
|
+
Author-email: Dominik Drexler <dominik.drexler@liu.se>
|
|
7
|
+
License-Expression: GPL-3.0-or-later
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: pooch>=1.8
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest; extra == 'test'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# planning-benchmarks
|
|
15
|
+
|
|
16
|
+
PDDL planning benchmark suites — classical, numeric, profiling, and generated
|
|
17
|
+
learning splits — published as the [`pypddl-datasets`](https://pypi.org/project/pypddl-datasets/)
|
|
18
|
+
Python package. The package itself is small: benchmark data is downloaded on
|
|
19
|
+
first use from the matching GitHub release and cached locally.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
pip install pypddl-datasets
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import pypddl_datasets as pb
|
|
29
|
+
|
|
30
|
+
pb.list_suites() # ['autoscale-agile-strips', ..., 'ipc-optimal-strips', ...]
|
|
31
|
+
|
|
32
|
+
task = pb.fetch_task("classical/tests/gripper/test-1.pddl")
|
|
33
|
+
task.domain_path # .../gripper/domain.pddl (correct also where instances
|
|
34
|
+
task.task_path # .../gripper/test-1.pddl carry their own domain files)
|
|
35
|
+
task.domain, task.problem # "classical/tests/gripper", "test-1.pddl" — display names
|
|
36
|
+
|
|
37
|
+
domain = pb.fetch_domain("classical/downward-benchmarks/gripper")
|
|
38
|
+
domain.path # the domain directory
|
|
39
|
+
domain.tasks # list[Task], downloaded once and cached
|
|
40
|
+
|
|
41
|
+
suite = pb.fetch_suite("ipc-optimal-strips") # Suite(path, domains)
|
|
42
|
+
for domain in suite.domains:
|
|
43
|
+
for task in domain.tasks:
|
|
44
|
+
run_planner(task.domain_path, task.task_path)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Most suites have a `-test` companion (e.g. `"ipc-optimal-strips-test"`) whose
|
|
48
|
+
entries are one representative task per domain — a cheap smoke run before
|
|
49
|
+
committing to a full suite. `pb.export_suite(suite, dest)` materializes a
|
|
50
|
+
suite as a plain directory tree for non-Python tools.
|
|
51
|
+
|
|
52
|
+
`pb.list_domains()` lists every individually fetchable domain. The cache
|
|
53
|
+
location defaults to the platform cache dir and can be overridden with the
|
|
54
|
+
`PYPDDL_DATASETS_CACHE` environment variable. On machines without internet
|
|
55
|
+
access, set `PYPDDL_DATASETS_DATA` to a local checkout's `data/` directory
|
|
56
|
+
and domains resolve there without downloading.
|
|
57
|
+
|
|
58
|
+
## Repository layout
|
|
59
|
+
|
|
60
|
+
- `src/pypddl_datasets/` — the package: fetch API, suite definitions, and the
|
|
61
|
+
instance generators (including the train/valid/test split configurations).
|
|
62
|
+
- `data/` — all benchmark data, organized as `<formalism>/<collection>/<domain>`
|
|
63
|
+
(`classical/`, `numeric/`). Not shipped in the package; released as a single
|
|
64
|
+
archive on `data-v*` GitHub releases, downloaded and unpacked once per
|
|
65
|
+
machine on first use.
|
|
66
|
+
- `data/classical/generated/<domain>-{train,valid,test}/` — fixed learning
|
|
67
|
+
splits produced by the generators. These committed instances are the
|
|
68
|
+
reproducibility contract; regenerate with
|
|
69
|
+
`python -m pypddl_datasets.generators.classical.<domain>.generate_instances`.
|
|
70
|
+
- `scripts/package_data.py` — turns `data/` into the byte-reproducible
|
|
71
|
+
`data.tar.gz` release archive.
|
|
72
|
+
- `validate.py` — parses every domain/problem with pypddl; must pass for a
|
|
73
|
+
data release to go out.
|
|
74
|
+
|
|
75
|
+
## Releasing
|
|
76
|
+
|
|
77
|
+
Data and package releases are decoupled; data releases are permanent
|
|
78
|
+
(published package versions pin them by tag).
|
|
79
|
+
|
|
80
|
+
1. **Data changed?** Tag and push `data-v<N>`. The workflow validates all
|
|
81
|
+
PDDL (aborting the release on failure), packages `data.tar.gz`, and
|
|
82
|
+
uploads it to the `data-v<N>` GitHub release. Copy the sha256 it prints
|
|
83
|
+
into `DATA_SHA256` (and bump `DATA_VERSION`) in
|
|
84
|
+
`src/pypddl_datasets/__init__.py`.
|
|
85
|
+
2. **Package release:** bump `version` in `pyproject.toml`, tag and push
|
|
86
|
+
`v<version>`. The workflow verifies the tag matches the version and that
|
|
87
|
+
the pinned data release exists with the pinned hash, then builds and
|
|
88
|
+
publishes to PyPI via trusted publishing. No assets are uploaded — code
|
|
89
|
+
releases cost nothing in storage.
|
|
90
|
+
|
|
91
|
+
## Contributing data
|
|
92
|
+
|
|
93
|
+
Clone with Git LFS (large instances are LFS-tracked):
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
git lfs install
|
|
97
|
+
git clone git@github.com:planning-and-learning/planning-benchmarks.git
|
|
98
|
+
cd planning-benchmarks && git lfs pull
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Track PDDL files larger than 10 MiB before staging them:
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
find data -type f -name '*.pddl' -size +10M -print0 | xargs -0 git lfs track --filename
|
|
105
|
+
git add .gitattributes data
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Domain directories must contain their `.pddl` files directly (that is how
|
|
109
|
+
`scripts/package_data.py` discovers them), and directory names must not
|
|
110
|
+
contain `--`. Run `pytest tests` to check suite definitions stay consistent.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# planning-benchmarks
|
|
2
|
+
|
|
3
|
+
PDDL planning benchmark suites — classical, numeric, profiling, and generated
|
|
4
|
+
learning splits — published as the [`pypddl-datasets`](https://pypi.org/project/pypddl-datasets/)
|
|
5
|
+
Python package. The package itself is small: benchmark data is downloaded on
|
|
6
|
+
first use from the matching GitHub release and cached locally.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pip install pypddl-datasets
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import pypddl_datasets as pb
|
|
16
|
+
|
|
17
|
+
pb.list_suites() # ['autoscale-agile-strips', ..., 'ipc-optimal-strips', ...]
|
|
18
|
+
|
|
19
|
+
task = pb.fetch_task("classical/tests/gripper/test-1.pddl")
|
|
20
|
+
task.domain_path # .../gripper/domain.pddl (correct also where instances
|
|
21
|
+
task.task_path # .../gripper/test-1.pddl carry their own domain files)
|
|
22
|
+
task.domain, task.problem # "classical/tests/gripper", "test-1.pddl" — display names
|
|
23
|
+
|
|
24
|
+
domain = pb.fetch_domain("classical/downward-benchmarks/gripper")
|
|
25
|
+
domain.path # the domain directory
|
|
26
|
+
domain.tasks # list[Task], downloaded once and cached
|
|
27
|
+
|
|
28
|
+
suite = pb.fetch_suite("ipc-optimal-strips") # Suite(path, domains)
|
|
29
|
+
for domain in suite.domains:
|
|
30
|
+
for task in domain.tasks:
|
|
31
|
+
run_planner(task.domain_path, task.task_path)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Most suites have a `-test` companion (e.g. `"ipc-optimal-strips-test"`) whose
|
|
35
|
+
entries are one representative task per domain — a cheap smoke run before
|
|
36
|
+
committing to a full suite. `pb.export_suite(suite, dest)` materializes a
|
|
37
|
+
suite as a plain directory tree for non-Python tools.
|
|
38
|
+
|
|
39
|
+
`pb.list_domains()` lists every individually fetchable domain. The cache
|
|
40
|
+
location defaults to the platform cache dir and can be overridden with the
|
|
41
|
+
`PYPDDL_DATASETS_CACHE` environment variable. On machines without internet
|
|
42
|
+
access, set `PYPDDL_DATASETS_DATA` to a local checkout's `data/` directory
|
|
43
|
+
and domains resolve there without downloading.
|
|
44
|
+
|
|
45
|
+
## Repository layout
|
|
46
|
+
|
|
47
|
+
- `src/pypddl_datasets/` — the package: fetch API, suite definitions, and the
|
|
48
|
+
instance generators (including the train/valid/test split configurations).
|
|
49
|
+
- `data/` — all benchmark data, organized as `<formalism>/<collection>/<domain>`
|
|
50
|
+
(`classical/`, `numeric/`). Not shipped in the package; released as a single
|
|
51
|
+
archive on `data-v*` GitHub releases, downloaded and unpacked once per
|
|
52
|
+
machine on first use.
|
|
53
|
+
- `data/classical/generated/<domain>-{train,valid,test}/` — fixed learning
|
|
54
|
+
splits produced by the generators. These committed instances are the
|
|
55
|
+
reproducibility contract; regenerate with
|
|
56
|
+
`python -m pypddl_datasets.generators.classical.<domain>.generate_instances`.
|
|
57
|
+
- `scripts/package_data.py` — turns `data/` into the byte-reproducible
|
|
58
|
+
`data.tar.gz` release archive.
|
|
59
|
+
- `validate.py` — parses every domain/problem with pypddl; must pass for a
|
|
60
|
+
data release to go out.
|
|
61
|
+
|
|
62
|
+
## Releasing
|
|
63
|
+
|
|
64
|
+
Data and package releases are decoupled; data releases are permanent
|
|
65
|
+
(published package versions pin them by tag).
|
|
66
|
+
|
|
67
|
+
1. **Data changed?** Tag and push `data-v<N>`. The workflow validates all
|
|
68
|
+
PDDL (aborting the release on failure), packages `data.tar.gz`, and
|
|
69
|
+
uploads it to the `data-v<N>` GitHub release. Copy the sha256 it prints
|
|
70
|
+
into `DATA_SHA256` (and bump `DATA_VERSION`) in
|
|
71
|
+
`src/pypddl_datasets/__init__.py`.
|
|
72
|
+
2. **Package release:** bump `version` in `pyproject.toml`, tag and push
|
|
73
|
+
`v<version>`. The workflow verifies the tag matches the version and that
|
|
74
|
+
the pinned data release exists with the pinned hash, then builds and
|
|
75
|
+
publishes to PyPI via trusted publishing. No assets are uploaded — code
|
|
76
|
+
releases cost nothing in storage.
|
|
77
|
+
|
|
78
|
+
## Contributing data
|
|
79
|
+
|
|
80
|
+
Clone with Git LFS (large instances are LFS-tracked):
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
git lfs install
|
|
84
|
+
git clone git@github.com:planning-and-learning/planning-benchmarks.git
|
|
85
|
+
cd planning-benchmarks && git lfs pull
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Track PDDL files larger than 10 MiB before staging them:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
find data -type f -name '*.pddl' -size +10M -print0 | xargs -0 git lfs track --filename
|
|
92
|
+
git add .gitattributes data
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Domain directories must contain their `.pddl` files directly (that is how
|
|
96
|
+
`scripts/package_data.py` discovers them), and directory names must not
|
|
97
|
+
contain `--`. Run `pytest tests` to check suite definitions stay consistent.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pypddl-datasets"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Download PDDL planning benchmark suites: classical, numeric, profiling, and learning splits."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "GPL-3.0-or-later"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Dominik Drexler", email = "dominik.drexler@liu.se" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"pooch>=1.8",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
test = ["pytest"]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/planning-and-learning/planning-benchmarks"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["src/pypddl_datasets"]
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.sdist]
|
|
27
|
+
only-include = ["src", "scripts", "README.md"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Package data/ into a single byte-reproducible data.tar.gz for upload as a
|
|
3
|
+
GitHub release asset, and print its sha256 (to pin in the package)."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import gzip
|
|
9
|
+
import hashlib
|
|
10
|
+
import tarfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
LFS_POINTER_PREFIX = b"version https://git-lfs.github.com/spec/v1"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def discover_domains(data_root: Path) -> list[Path]:
|
|
17
|
+
"""A domain is a maximal directory containing at least one .pddl file directly."""
|
|
18
|
+
domains: list[Path] = []
|
|
19
|
+
|
|
20
|
+
def walk(directory: Path) -> None:
|
|
21
|
+
if any(child.suffix == ".pddl" for child in directory.iterdir() if child.is_file()):
|
|
22
|
+
domains.append(directory)
|
|
23
|
+
return
|
|
24
|
+
for child in directory.iterdir():
|
|
25
|
+
if child.is_dir():
|
|
26
|
+
walk(child)
|
|
27
|
+
|
|
28
|
+
walk(data_root)
|
|
29
|
+
return sorted(domains)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def assert_no_lfs_pointers(data_root: Path) -> None:
|
|
33
|
+
for path in sorted(data_root.rglob("*")):
|
|
34
|
+
if path.is_file():
|
|
35
|
+
with path.open("rb") as stream:
|
|
36
|
+
if stream.read(len(LFS_POINTER_PREFIX)) == LFS_POINTER_PREFIX:
|
|
37
|
+
raise SystemExit(f"LFS pointer file at {path}; run 'git lfs pull' first")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def write_archive(data_root: Path, domains: list[Path], tar_path: Path) -> None:
|
|
41
|
+
"""Byte-reproducible tar.gz (fixed metadata, no gzip timestamp): re-running
|
|
42
|
+
produces identical bytes, so the pinned sha256 stays valid. Only files
|
|
43
|
+
inside domain directories are included, so stray files under the data
|
|
44
|
+
root (e.g. tool outputs) can never leak into the archive."""
|
|
45
|
+
files = sorted(path for domain in domains for path in domain.rglob("*") if path.is_file())
|
|
46
|
+
with open(tar_path, "wb") as out:
|
|
47
|
+
with gzip.GzipFile(filename="", fileobj=out, mode="wb", mtime=0) as stream:
|
|
48
|
+
with tarfile.open(fileobj=stream, mode="w") as tar:
|
|
49
|
+
for path in files:
|
|
50
|
+
info = tar.gettarinfo(path, arcname=path.relative_to(data_root).as_posix())
|
|
51
|
+
info.uid = info.gid = 0
|
|
52
|
+
info.uname = info.gname = ""
|
|
53
|
+
info.mtime = 0
|
|
54
|
+
info.mode = 0o644
|
|
55
|
+
with path.open("rb") as source:
|
|
56
|
+
tar.addfile(info, source)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> int:
|
|
60
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
61
|
+
parser.add_argument("--data-root", type=Path, default=Path(__file__).resolve().parents[1] / "data")
|
|
62
|
+
parser.add_argument("--output", type=Path, default=Path("dist-data/data.tar.gz"))
|
|
63
|
+
parser.add_argument("--dry-run", action="store_true", help="List domains without writing anything.")
|
|
64
|
+
args = parser.parse_args()
|
|
65
|
+
|
|
66
|
+
domains = discover_domains(args.data_root)
|
|
67
|
+
if not domains:
|
|
68
|
+
raise SystemExit("no domains found")
|
|
69
|
+
|
|
70
|
+
if args.dry_run:
|
|
71
|
+
for domain in domains:
|
|
72
|
+
print(domain.relative_to(args.data_root).as_posix())
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
assert_no_lfs_pointers(args.data_root)
|
|
76
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
write_archive(args.data_root, domains, args.output)
|
|
78
|
+
digest = hashlib.sha256(args.output.read_bytes()).hexdigest()
|
|
79
|
+
print(f"[package-data] {args.output} ({args.output.stat().st_size} bytes, {len(domains)} domains)")
|
|
80
|
+
print(f"[package-data] sha256: {digest}")
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Download and cache the planning-benchmarks PDDL suites.
|
|
2
|
+
|
|
3
|
+
Data is not shipped with this package: the first fetch downloads the pinned
|
|
4
|
+
data release (one archive) and unpacks it into the local cache; every later
|
|
5
|
+
call reuses that extraction. Override the cache location with the
|
|
6
|
+
PYPDDL_DATASETS_CACHE environment variable, or point PYPDDL_DATASETS_DATA at
|
|
7
|
+
a local checkout's data/ directory to use it directly (no download).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import functools
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import shutil
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import pooch
|
|
19
|
+
|
|
20
|
+
from .suites import SUITES
|
|
21
|
+
|
|
22
|
+
__version__ = "0.0.1"
|
|
23
|
+
|
|
24
|
+
# The pinned data release: updated when a new data-v* release is cut
|
|
25
|
+
# (its workflow prints the archive's sha256). Empty sha256 = development
|
|
26
|
+
# build without a pinned release; only PYPDDL_DATASETS_DATA works then.
|
|
27
|
+
DATA_VERSION = "data-v0.0.1"
|
|
28
|
+
DATA_SHA256 = "54b80e0f8862e6401632a8cc5e7465e25ef7dfacad0823deeb809328aa6e10e3"
|
|
29
|
+
|
|
30
|
+
_ARCHIVE = "data.tar.gz" # single asset on the data release
|
|
31
|
+
|
|
32
|
+
_CACHE = Path(os.environ.get("PYPDDL_DATASETS_CACHE", pooch.os_cache("pypddl-datasets"))) / DATA_VERSION
|
|
33
|
+
|
|
34
|
+
_POOCH = pooch.create(
|
|
35
|
+
path=_CACHE,
|
|
36
|
+
base_url=f"https://github.com/planning-and-learning/planning-benchmarks/releases/download/{DATA_VERSION}/",
|
|
37
|
+
registry={_ARCHIVE: f"sha256:{DATA_SHA256}"},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Task:
|
|
42
|
+
"""A single planning task of a domain: display names (domain, problem),
|
|
43
|
+
the domain directory it belongs to, its domain file, and its problem
|
|
44
|
+
file."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, domain: str, problem: str, path: Path, domain_path: Path, task_path: Path) -> None:
|
|
47
|
+
self._domain = domain
|
|
48
|
+
self._problem = problem
|
|
49
|
+
self._path = path
|
|
50
|
+
self._domain_path = domain_path
|
|
51
|
+
self._task_path = task_path
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def domain(self) -> str:
|
|
55
|
+
return self._domain
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def problem(self) -> str:
|
|
59
|
+
return self._problem
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def path(self) -> Path:
|
|
63
|
+
return self._path
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def domain_path(self) -> Path:
|
|
67
|
+
return self._domain_path
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def task_path(self) -> Path:
|
|
71
|
+
return self._task_path
|
|
72
|
+
|
|
73
|
+
def __repr__(self) -> str:
|
|
74
|
+
return f"Task(domain={self._domain!r}, problem={self._problem!r})"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Domain:
|
|
78
|
+
"""A benchmark domain: its directory and all of its tasks, each paired
|
|
79
|
+
with the correct domain file (which is per-instance in some collections,
|
|
80
|
+
e.g. airport)."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, path: Path, tasks: list[Task]) -> None:
|
|
83
|
+
self._path = path
|
|
84
|
+
self._tasks = tasks
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def path(self) -> Path:
|
|
88
|
+
return self._path
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def tasks(self) -> list[Task]:
|
|
92
|
+
return self._tasks
|
|
93
|
+
|
|
94
|
+
def __repr__(self) -> str:
|
|
95
|
+
return f"Domain(path={self._path!r}, tasks={self._tasks!r})"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Suite:
|
|
99
|
+
"""A named benchmark suite: the root directory its domains live under
|
|
100
|
+
and the fetched domains themselves."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, path: Path, domains: list[Domain]) -> None:
|
|
103
|
+
self._path = path
|
|
104
|
+
self._domains = domains
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def path(self) -> Path:
|
|
108
|
+
return self._path
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def domains(self) -> list[Domain]:
|
|
112
|
+
return self._domains
|
|
113
|
+
|
|
114
|
+
def __repr__(self) -> str:
|
|
115
|
+
return f"Suite(path={self._path!r}, domains={self._domains!r})"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def list_suites() -> list[str]:
|
|
119
|
+
"""Names accepted by fetch_suite()."""
|
|
120
|
+
return sorted(SUITES)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def list_domains() -> list[str]:
|
|
124
|
+
"""Domain names accepted by fetch_domain() and fetch_task(); triggers the
|
|
125
|
+
one-time data download when no local data is configured. A domain is a
|
|
126
|
+
maximal directory containing .pddl files directly."""
|
|
127
|
+
root = _data_root()
|
|
128
|
+
domains: list[str] = []
|
|
129
|
+
|
|
130
|
+
def walk(directory: Path) -> None:
|
|
131
|
+
if any(child.suffix == ".pddl" for child in directory.iterdir() if child.is_file()):
|
|
132
|
+
domains.append(directory.relative_to(root).as_posix())
|
|
133
|
+
return
|
|
134
|
+
for child in directory.iterdir():
|
|
135
|
+
if child.is_dir():
|
|
136
|
+
walk(child)
|
|
137
|
+
|
|
138
|
+
walk(root)
|
|
139
|
+
return sorted(domains)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def fetch_domain(name: str) -> Domain:
|
|
143
|
+
"""Fetch a domain such as "classical/downward-benchmarks/gripper",
|
|
144
|
+
downloading it if necessary."""
|
|
145
|
+
directory = _fetch_directory(name)
|
|
146
|
+
return Domain(directory, _pair_tasks(name, directory))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def fetch_task(name: str) -> Task:
|
|
150
|
+
"""Fetch one task by its full name, e.g.
|
|
151
|
+
fetch_task("classical/tests/gripper/test-1.pddl"). The split between
|
|
152
|
+
domain and task file is unambiguous because domains are never nested."""
|
|
153
|
+
domain, task = _split_task_name(name)
|
|
154
|
+
directory = _fetch_directory(domain)
|
|
155
|
+
for candidate in _pair_tasks(domain, directory):
|
|
156
|
+
if task in (candidate.problem, candidate.task_path.name):
|
|
157
|
+
return candidate
|
|
158
|
+
raise KeyError(f"no task {task!r} in domain {domain!r}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def fetch_suite(suite: str) -> Suite:
|
|
162
|
+
"""Fetch all entries of a named suite; see list_suites(). A plain domain
|
|
163
|
+
entry yields the whole domain; a "<domain>:<instance>" entry (as used by
|
|
164
|
+
the "-test" suites) yields a Domain restricted to that single task."""
|
|
165
|
+
if suite not in SUITES:
|
|
166
|
+
raise KeyError(f"unknown suite {suite!r}; see list_suites()")
|
|
167
|
+
domains = []
|
|
168
|
+
for entry in SUITES[suite]:
|
|
169
|
+
name, _, instance = entry.partition(":")
|
|
170
|
+
if instance:
|
|
171
|
+
task = fetch_task(f"{name}/{instance}")
|
|
172
|
+
domains.append(Domain(task.path, [task]))
|
|
173
|
+
else:
|
|
174
|
+
domains.append(fetch_domain(name))
|
|
175
|
+
return Suite(_data_root(), domains)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def export_suite(suite: str, dest: str | Path) -> list[Path]:
|
|
179
|
+
"""Copy all domains of a named suite into dest/<domain path>/, for tools
|
|
180
|
+
that expect benchmark files in a fixed directory tree."""
|
|
181
|
+
if suite not in SUITES:
|
|
182
|
+
raise KeyError(f"unknown suite {suite!r}; see list_suites()")
|
|
183
|
+
paths = []
|
|
184
|
+
for entry in SUITES[suite]:
|
|
185
|
+
name = entry.partition(":")[0]
|
|
186
|
+
target = Path(dest) / name
|
|
187
|
+
shutil.copytree(_fetch_directory(name), target, dirs_exist_ok=True)
|
|
188
|
+
paths.append(target)
|
|
189
|
+
return paths
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _data_root() -> Path:
|
|
193
|
+
"""The root directory the data lives under, downloading and unpacking the
|
|
194
|
+
pinned data release once if necessary.
|
|
195
|
+
|
|
196
|
+
If PYPDDL_DATASETS_DATA is set (e.g. to a local checkout's data/ dir on
|
|
197
|
+
machines without internet access), it is used directly instead."""
|
|
198
|
+
local_root = os.environ.get("PYPDDL_DATASETS_DATA")
|
|
199
|
+
if local_root:
|
|
200
|
+
return Path(local_root)
|
|
201
|
+
return _fetched_data_root()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@functools.cache
|
|
205
|
+
def _fetched_data_root() -> Path:
|
|
206
|
+
"""Download and unpack the pinned data release; once per machine on disk,
|
|
207
|
+
and the (re-)verification of the cached archive once per process."""
|
|
208
|
+
if not DATA_SHA256:
|
|
209
|
+
raise KeyError(
|
|
210
|
+
"this pypddl-datasets build has no pinned data release (development "
|
|
211
|
+
"build); set PYPDDL_DATASETS_DATA to a local checkout's data/ "
|
|
212
|
+
"directory or install a released version"
|
|
213
|
+
)
|
|
214
|
+
_POOCH.fetch(_ARCHIVE, processor=pooch.Untar(extract_dir="data"))
|
|
215
|
+
return _CACHE / "data"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _split_task_name(name: str) -> tuple[str, str]:
|
|
219
|
+
"""Split "classical/tests/gripper/test-1.pddl" into domain and task part.
|
|
220
|
+
Unique because domain directories are maximal (never nested)."""
|
|
221
|
+
root = _data_root()
|
|
222
|
+
parts = name.split("/")
|
|
223
|
+
for index in range(1, len(parts)):
|
|
224
|
+
domain = "/".join(parts[:index])
|
|
225
|
+
directory = root / domain
|
|
226
|
+
if directory.is_dir() and any(directory.glob("*.pddl")):
|
|
227
|
+
return domain, "/".join(parts[index:])
|
|
228
|
+
raise KeyError(f"no domain found in task name {name!r}; see list_domains()")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _fetch_directory(name: str) -> Path:
|
|
232
|
+
"""The local directory of a domain, downloading the data if necessary."""
|
|
233
|
+
path = _data_root() / name
|
|
234
|
+
if not path.is_dir() or not any(path.glob("*.pddl")):
|
|
235
|
+
raise KeyError(f"unknown domain {name!r}; see list_domains()")
|
|
236
|
+
return path
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# Domain/problem pairing, following the same rules as the repository's validate.py.
|
|
240
|
+
|
|
241
|
+
_DECLARATION = re.compile(r"\(\s*define\s*\(\s*(domain|problem)\s+([^\s()]+)", re.IGNORECASE)
|
|
242
|
+
_DOMAIN_REFERENCE = re.compile(r"\(\s*:domain\s+([^\s()]+)", re.IGNORECASE)
|
|
243
|
+
_HEAD_BYTES = 65536
|
|
244
|
+
_LFS_POINTER_PREFIX = "version https://git-lfs.github.com/spec/v1"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _declaration(path: Path) -> tuple[str, str | None] | None:
|
|
248
|
+
"""("domain"|"problem", declared name), classified by file content."""
|
|
249
|
+
with path.open("r", encoding="utf-8", errors="replace") as stream:
|
|
250
|
+
head = stream.read(_HEAD_BYTES)
|
|
251
|
+
if head.startswith(_LFS_POINTER_PREFIX):
|
|
252
|
+
# LFS pointer stub (checkout without `git lfs pull`): classify by
|
|
253
|
+
# file name so pairing matches a full checkout.
|
|
254
|
+
return ("domain" if "domain" in path.name else "problem", None)
|
|
255
|
+
text = re.sub(r";[^\n]*", "", head)
|
|
256
|
+
match = _DECLARATION.search(text)
|
|
257
|
+
if match is None and stream.read(1):
|
|
258
|
+
text = re.sub(r";[^\n]*", "", text + stream.read())
|
|
259
|
+
match = _DECLARATION.search(text)
|
|
260
|
+
return (match.group(1).lower(), match.group(2)) if match else None
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _pair_tasks(domain: str, directory: Path) -> list[Task]:
|
|
264
|
+
domain_files: dict[Path, str] = {}
|
|
265
|
+
problem_files: list[Path] = []
|
|
266
|
+
for path in sorted(directory.rglob("*.pddl")):
|
|
267
|
+
declared = _declaration(path)
|
|
268
|
+
if declared is None:
|
|
269
|
+
continue
|
|
270
|
+
kind, name = declared
|
|
271
|
+
if kind == "domain":
|
|
272
|
+
domain_files[path] = name.lower() if name else ""
|
|
273
|
+
else:
|
|
274
|
+
problem_files.append(path)
|
|
275
|
+
return [
|
|
276
|
+
Task(
|
|
277
|
+
domain,
|
|
278
|
+
problem.relative_to(directory).as_posix(),
|
|
279
|
+
directory,
|
|
280
|
+
_resolve_domain(problem, domain_files, directory),
|
|
281
|
+
problem,
|
|
282
|
+
)
|
|
283
|
+
for problem in problem_files
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _resolve_domain(problem: Path, domain_files: dict[Path, str], root: Path) -> Path:
|
|
288
|
+
stem = problem.stem
|
|
289
|
+
candidates = (
|
|
290
|
+
"domain.pddl",
|
|
291
|
+
f"{stem}-domain{problem.suffix}",
|
|
292
|
+
f"{stem[:3]}-domain.pddl",
|
|
293
|
+
f"domain_{problem.name}",
|
|
294
|
+
f"domain-{problem.name}",
|
|
295
|
+
)
|
|
296
|
+
directory = problem.parent
|
|
297
|
+
while True:
|
|
298
|
+
for candidate in candidates:
|
|
299
|
+
if directory / candidate in domain_files:
|
|
300
|
+
return directory / candidate
|
|
301
|
+
if directory == root:
|
|
302
|
+
break
|
|
303
|
+
directory = directory.parent
|
|
304
|
+
if len(domain_files) == 1:
|
|
305
|
+
return next(iter(domain_files))
|
|
306
|
+
reference = _DOMAIN_REFERENCE.search(problem.read_text(encoding="utf-8", errors="replace"))
|
|
307
|
+
if reference:
|
|
308
|
+
matches = [path for path, name in domain_files.items() if name == reference.group(1).lower()]
|
|
309
|
+
if len(matches) == 1:
|
|
310
|
+
return matches[0]
|
|
311
|
+
raise ValueError(f"cannot resolve domain file for {problem}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generators
|
|
2
|
+
|
|
3
|
+
A fully Python variant of [AI-planning/pddl-generators](https://github.com/AI-Planning/pddl-generators).
|
|
4
|
+
|
|
5
|
+
# Polynomially Recognizable Deadend Domains
|
|
6
|
+
|
|
7
|
+
- barman
|
|
8
|
+
- blocks_3
|
|
9
|
+
- childsnack
|
|
10
|
+
- hiking
|
|
11
|
+
- hiking_binary
|
|
12
|
+
- nomystery
|
|
13
|
+
- parcprinter
|
|
14
|
+
- rovers
|
|
15
|
+
- schedule
|
|
16
|
+
- tpp
|
|
17
|
+
- spanner
|
|
18
|
+
- woodworking
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Planning benchmark generators."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Classical planning benchmark generators."""
|