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.
Files changed (89) hide show
  1. pypddl_datasets-0.0.1/.gitignore +7 -0
  2. pypddl_datasets-0.0.1/PKG-INFO +110 -0
  3. pypddl_datasets-0.0.1/README.md +97 -0
  4. pypddl_datasets-0.0.1/pyproject.toml +27 -0
  5. pypddl_datasets-0.0.1/scripts/package_data.py +85 -0
  6. pypddl_datasets-0.0.1/src/pypddl_datasets/__init__.py +311 -0
  7. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/README.md +18 -0
  8. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/__init__.py +1 -0
  9. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/__init__.py +1 -0
  10. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/barman/domain.pddl +170 -0
  11. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/barman/generate_instances.py +97 -0
  12. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/barman/generator.py +145 -0
  13. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/blocks_3/domain.pddl +24 -0
  14. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/blocks_3/generate_instances.py +83 -0
  15. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/blocks_3/generator.py +83 -0
  16. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/blocks_4/domain.pddl +31 -0
  17. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/blocks_4/generate_instances.py +83 -0
  18. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/blocks_4/generator.py +83 -0
  19. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/childsnack/domain.pddl +109 -0
  20. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/childsnack/generate_instances.py +83 -0
  21. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/childsnack/generator.py +96 -0
  22. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/delivery/__init__.py +5 -0
  23. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/delivery/domain.pddl +42 -0
  24. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/delivery/generate_instances.py +83 -0
  25. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/delivery/generator.py +95 -0
  26. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/driverlog/domain.pddl +79 -0
  27. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/driverlog/generate_instances.py +101 -0
  28. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/driverlog/generator.py +189 -0
  29. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/ferry/README.md +14 -0
  30. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/ferry/__init__.py +5 -0
  31. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/ferry/domain.pddl +32 -0
  32. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/ferry/generate_instances.py +83 -0
  33. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/ferry/generator.py +79 -0
  34. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/floortile/__init__.py +5 -0
  35. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/floortile/domain.pddl +82 -0
  36. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/floortile/generate_instances.py +77 -0
  37. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/floortile/generator.py +105 -0
  38. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/goldminer/__init__.py +5 -0
  39. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/goldminer/domain.pddl +97 -0
  40. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/goldminer/generate_instances.py +77 -0
  41. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/goldminer/generator.py +128 -0
  42. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/grid/__init__.py +5 -0
  43. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/grid/domain.pddl +52 -0
  44. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/grid/generate_instances.py +77 -0
  45. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/grid/generator.py +157 -0
  46. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/gripper/README.md +9 -0
  47. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/gripper/__init__.py +5 -0
  48. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/gripper/domain.pddl +36 -0
  49. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/gripper/generate_instances.py +82 -0
  50. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/gripper/generator.py +52 -0
  51. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking/__init__.py +5 -0
  52. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking/domain.pddl +83 -0
  53. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking/generate_instances.py +82 -0
  54. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking/generator.py +92 -0
  55. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking_binary/__init__.py +5 -0
  56. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking_binary/domain.pddl +137 -0
  57. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking_binary/generate_instances.py +77 -0
  58. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/hiking_binary/generator.py +93 -0
  59. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/logistics/__init__.py +5 -0
  60. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/logistics/domain.pddl +83 -0
  61. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/logistics/generate_instances.py +91 -0
  62. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/logistics/generator.py +139 -0
  63. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/miconic/__init__.py +5 -0
  64. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/miconic/domain.pddl +60 -0
  65. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/miconic/generate_instances.py +82 -0
  66. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/miconic/generator.c +954 -0
  67. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/miconic/generator.py +68 -0
  68. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/nomystery/domain.pddl +68 -0
  69. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/nomystery/generator.py +337 -0
  70. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/rovers/domain.pddl +92 -0
  71. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/rovers/generator.py +384 -0
  72. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/satellite/__init__.py +5 -0
  73. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/satellite/domain.pddl +50 -0
  74. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/satellite/generate_instances.py +79 -0
  75. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/satellite/generator.py +111 -0
  76. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/schedule/domain.pddl +202 -0
  77. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/schedule/generator.py +227 -0
  78. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/spanner/domain.pddl +36 -0
  79. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/spanner/generator.py +91 -0
  80. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/tpp/README.md +2 -0
  81. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/tpp/__init__.py +5 -0
  82. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/tpp/domain.pddl +65 -0
  83. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/tpp/generate_instances.py +82 -0
  84. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/tpp/generator.py +106 -0
  85. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/visitall/domain.pddl +40 -0
  86. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/visitall/generator.py +142 -0
  87. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/woodworking/domain.pddl +300 -0
  88. pypddl_datasets-0.0.1/src/pypddl_datasets/generators/classical/woodworking/generator.py +431 -0
  89. pypddl_datasets-0.0.1/src/pypddl_datasets/suites.py +1023 -0
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ dist/
4
+ *.egg-info/
5
+ .venv/
6
+ validate.error.json
7
+ validate.sucess.json
@@ -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."""