Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions .docs/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
# Research notes
# Documentation

Working notes for the learning-for-planning line in `jupyddl`. These are
research documents, not user documentation — the user-facing description of
`jupyddl learn` is in the README and the module docstrings.
Everything that is not the README lives here: the research notes for the
learning-for-planning line, the release runbook, and every image, video and
measurement cache under [`assets/`](assets).

These notes are research documents, not user documentation — the user-facing
description of `jupyddl learn` is in the README and the module docstrings.

The two files GitHub reads for its own community features,
`CONTRIBUTING.md` and `CODE_OF_CONDUCT.md`, are in `.github/` instead: GitHub
only looks for them in the repository root, `.github/` or `docs/`, and would
not find them here.

| Note | What it covers |
|---|---|
| [learned-heuristics.md](learned-heuristics.md) | The imitation stage: prior work, the design we chose, measured results on three domains, and an analysis of the one domain where it loses badly |
| [rl-for-search.md](rl-for-search.md) | The reinforcement stage: the MDP that "minimise expansions" corresponds to, why the obvious policy gradient is hard here, and what we do instead |
| [roadmap.md](roadmap.md) | What to build next, ordered by expected value, with the experiment that would settle each |
| [RELEASING.md](RELEASING.md) | How a release is cut, and the one-time PyPI trusted-publisher setup only a maintainer can do |

## The one-paragraph version

Expand Down Expand Up @@ -36,16 +45,16 @@ edited out.

## The video

[`promo/jupyddl-rl.mp4`](../promo/jupyddl-rl.mp4) is a 97-second tour of this
[`assets/jupyddl-rl.mp4`](assets/jupyddl-rl.mp4) is a 97-second tour of this
work. Like the main promo it measures everything at render time — it trains,
reinforces, and re-runs both failure modes — so it cannot drift from these
notes. Rebuild it with:

```bash
python tools/make_learn_promo.py --cache promo/rl-data.json -o promo/jupyddl-rl.mp4
python tools/make_learn_promo.py --cache .docs/assets/rl-data.json -o .docs/assets/jupyddl-rl.mp4
```

`promo/rl-data.json` is the cached measurement pass; delete it to re-measure.
`assets/rl-data.json` is the cached measurement pass; delete it to re-measure.

## Reproducing everything here

Expand Down
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ name: release
#
# Authentication is PyPI **Trusted Publishing** (OIDC), so there is no API
# token in this repository's secrets to leak or rotate. That requires a
# one-time setup on PyPI — see `docs/RELEASING.md`. Until it is done the
# one-time setup on PyPI — see `.docs/RELEASING.md`. Until it is done the
# `pypi` job fails and everything before it still succeeds, so a tag never
# leaves you with half a release and no artifacts.
#
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,4 @@ web/vendor/
# Generated galleries and animations
gallery/
*.mp4
!promo/*.mp4
!.docs/assets/*.mp4
17 changes: 14 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ native build step, and the core has zero runtime dependencies.
with `pyproject.toml` or a `__version__` that disagrees with either, runs
`twine check --strict`, installs the wheel clean and plans with it, publishes
to PyPI over OIDC (no stored token), then cuts the GitHub Release from the
changelog section for that version. `docs/RELEASING.md` is the runbook,
changelog section for that version. `.docs/RELEASING.md` is the runbook,
including the one-time PyPI trusted-publisher setup only a maintainer can do.
**Bump the version in two places** — `pyproject.toml` and
`jupyddl/__init__.py` — and rebuild `web/dist`, which carries it too.
Expand Down Expand Up @@ -77,15 +77,26 @@ native build step, and the core has zero runtime dependencies.
- `web/` — the Pyodide playground; `tools/build_web.py` bundles the package
sources and demos into `web/dist` (committed). It also writes
`capabilities.json` (the registries) and `research.json` (distilled from
`promo/rl-data.json`, so the page and the RL video quote one measured run).
`.docs/assets/rl-data.json`, so the page and the RL video quote one measured run).
Those two are rendered **before** Pyodide loads — the app shell is never
hidden, and only the run controls are gated on `state.ready` — so a stale
bundle briefly states something untrue rather than merely lagging.
`tests/test_web_bundle.py` pins both.
- `tools/make_promo.py` — renders the main promo video from measured runs.
- `tools/make_learn_promo.py` — the learned-heuristic/RL video. It re-measures
everything including both failure modes, so it cannot drift from `.docs/`;
`promo/rl-data.json` caches the pass, delete it to re-measure.
`.docs/assets/rl-data.json` caches the pass, delete it to re-measure.
- **`.docs/` is the only documentation directory.** Research notes and the
release runbook sit at its top level; every image, video and measurement
cache goes in `.docs/assets/`. There is no `docs/` or `promo/` — they were
merged in because three directories with no rule between them meant every
new file was a guess. The two exceptions live in `.github/`:
`CONTRIBUTING.md` and `CODE_OF_CONDUCT.md`, which GitHub only recognises in
the root, `.github/` or `docs/`.
The sdist excludes `.docs/assets/*.png` and `*.mp4` **by extension, not by
directory**, because `rl-data.json` sits beside them and
`tests/test_web_bundle.py` reads it — excluding the directory ships an sdist
whose own suite fails.

### The condition pipeline
Conditions are a **formula tree in negation normal form**: `parse_condition`
Expand Down
22 changes: 20 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed
- **Documentation lives in one place: `.docs/`.** It was spread over three
directories — `docs/` for the contributor files and the README charts,
`promo/` for the videos and screenshots, `.docs/` for the research notes —
with no rule saying which took what, so every new file was a guess. The
research notes, the release runbook and every image, video and measurement
cache are now under `.docs/`, with binaries in `.docs/assets/`. This also
matches `pymapf`, the sibling project.

Paths quoted in the released sections below are the ones that were correct
at the time. `promo/rl-data.json` is now `.docs/assets/rl-data.json`, and
`docs/images/` is now `.docs/assets/`.

`CONTRIBUTING.md` and `CODE_OF_CONDUCT.md` moved to `.github/` rather than
`.docs/`: GitHub only recognises those two files in the repository root,
`.github/` or `docs/`, so filing them under `.docs/` would have silently
dropped the contributing link on the issue and pull-request forms.

## [2.4.0]

### Added
Expand All @@ -15,8 +33,8 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
measured run: imitation against `hff` and `goalcount`, the per-instance
spread behind the mean, the logistics loss and why the feature space causes
it, and the three claims that turned out to be wrong. Its numbers are built
from `promo/rl-data.json` — the same cache the RL video renders from — so the
page and the video cannot drift apart, and a test pins them together.
from `promo/rl-data.json` — the same cache the RL video renders from — so
the page and the video cannot drift apart, and a test pins them together.

### Fixed
- **Reading the workbench no longer costs a 10 MB download.** `<main>` was
Expand Down
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ classical-to-SOTA planners, heuristics, benchmarking — and a search you can
actually *watch*.

**[▶ Open the workbench](https://openplan-labs.github.io/PythonPDDL/)** ·
[Watch the 105-second tour](promo/jupyddl-promo.mp4)
[Watch the 105-second tour](.docs/assets/jupyddl-promo.mp4)

The repository is `PythonPDDL`; the package is `jupyddl`. That is what you
install and what you import.
Expand All @@ -33,8 +33,8 @@ is trivial to install, embed, teach with, and build on.
<div align="center">

<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/comparison-dark.png">
<img src="docs/images/comparison.png" alt="Four planners compared on an 8-block blocksworld instance: nodes expanded on a log scale, runtime, plan cost, and the heuristic estimate along the search." width="880">
<source media="(prefers-color-scheme: dark)" srcset=".docs/assets/comparison-dark.png">
<img src=".docs/assets/comparison.png" alt="Four planners compared on an 8-block blocksworld instance: nodes expanded on a log scale, runtime, plan cost, and the heuristic estimate along the search." width="880">
</picture>

</div>
Expand Down Expand Up @@ -203,8 +203,8 @@ returns exactly the same plan and statistics as an unobserved one.)
<div align="center">

<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/search-progress-dark.png">
<img src="docs/images/search-progress.png" alt="Four panels showing an A*/LM-cut search on the gripper instance: f/g/h cost estimates, frontier size, expansion depth, and cumulative nodes over time." width="880">
<source media="(prefers-color-scheme: dark)" srcset=".docs/assets/search-progress-dark.png">
<img src=".docs/assets/search-progress.png" alt="Four panels showing an A*/LM-cut search on the gripper instance: f/g/h cost estimates, frontier size, expansion depth, and cumulative nodes over time." width="880">
</picture>

</div>
Expand All @@ -216,8 +216,8 @@ blind one fills the whole disc:
<div align="center">

<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/wavefront-dark.png">
<img src="docs/images/wavefront.png" alt="Radial chart of a breadth-first search: thousands of expanded nodes arranged in concentric rings by depth, coloured by heuristic value." width="620">
<source media="(prefers-color-scheme: dark)" srcset=".docs/assets/wavefront-dark.png">
<img src=".docs/assets/wavefront.png" alt="Radial chart of a breadth-first search: thousands of expanded nodes arranged in concentric rings by depth, coloured by heuristic value." width="620">
</picture>

</div>
Expand Down Expand Up @@ -281,7 +281,7 @@ never costs a 10 MB download.

<div align="center">

<img src="promo/workbench-dark.png" alt="The jupyddl workbench: a PDDL editor beside live cost-estimate charts, a radial search wavefront, and the resulting validated plan." width="880">
<img src=".docs/assets/workbench-dark.png" alt="The jupyddl workbench: a PDDL editor beside live cost-estimate charts, a radial search wavefront, and the resulting validated plan." width="880">

</div>

Expand Down Expand Up @@ -350,7 +350,7 @@ seen, against greedy best-first search:

Nearly four times fewer expansions than `hff` and about fifteen times faster,
because the network is a thousand multiply-adds and `hff` is a relaxed-plan
extraction. These are read from `promo/rl-data.json`, the cache the published
extraction. These are read from `.docs/assets/rl-data.json`, the cache the published
[Research view](https://openplan-labs.github.io/PythonPDDL/) renders from, so
the page and this table cannot drift apart.

Expand All @@ -359,7 +359,7 @@ exact rather than mysterious: that domain has two predicates, so the feature
vector cannot tell *which* package is where, only how many are somewhere. That
result, the RL formulation, and what to build next are written up in
[`.docs/`](.docs/), and there is a
[97-second tour of the RL half](promo/jupyddl-rl.mp4) — including the two
[97-second tour of the RL half](.docs/assets/jupyddl-rl.mp4) — including the two
measurement mistakes that shaped the design.

Read the 137 as a mean over a heavy tail: nine of the ten held-out instances sit
Expand Down Expand Up @@ -420,8 +420,8 @@ jupyddl benchmark demos --planners astar,gbfs,ehc,bfs --heuristic hff \
<div align="center">

<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/benchmark-dark.png">
<img src="docs/images/benchmark.png" alt="Benchmark dashboard: coverage, total nodes expanded on a log scale, total runtime, and a per-instance heatmap of nodes expanded across six demo instances and five planner configurations." width="960">
<source media="(prefers-color-scheme: dark)" srcset=".docs/assets/benchmark-dark.png">
<img src=".docs/assets/benchmark.png" alt="Benchmark dashboard: coverage, total nodes expanded on a log scale, total runtime, and a per-instance heatmap of nodes expanded across six demo instances and five planner configurations." width="960">
</picture>

</div>
Expand Down Expand Up @@ -588,8 +588,8 @@ Regenerating the media:

```bash
python tools/build_web.py # playground bundle
python tools/make_promo.py -o promo/jupyddl-promo.mp4 \
--screenshot promo/workbench-dark.png # the promo video
python tools/make_promo.py -o .docs/assets/jupyddl-promo.mp4 \
--screenshot .docs/assets/workbench-dark.png # the promo video
```

Every number in the promo video is measured at render time by running the real
Expand Down
11 changes: 7 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,16 @@ packages = ["jupyddl"]
# An sdist should carry what is needed to build and test the package, not the
# project's marketing. Left to its defaults hatchling packed the promo video
# and every screenshot and chart, making a 6.6 MB sdist against a 105 KB wheel.
# `web/` stays: `tests/test_web_bundle.py` reads it, so dropping it would leave
# an sdist whose own test suite fails.
#
# The media under `.docs/assets/` is excluded by extension rather than by
# excluding the directory, because `rl-data.json` lives beside it and
# `tests/test_web_bundle.py` reads it — as does `web/`, which stays for the
# same reason. Dropping either leaves an sdist whose own test suite fails.
[tool.hatch.build.targets.sdist]
exclude = [
".github/**",
"docs/images/**",
"promo/**",
".docs/assets/*.png",
".docs/assets/*.mp4",
"logs/**",
]

Expand Down
47 changes: 45 additions & 2 deletions tests/test_web_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def test_capabilities_bundle_agrees_with_the_registries():
def test_research_bundle_quotes_the_measured_run():
"""The Research view must not invent numbers.

``collect_research`` distils ``promo/rl-data.json`` — the cache the RL
``collect_research`` distils ``.docs/assets/rl-data.json`` — the cache the RL
video renders from — so page and video quote one measured run and cannot
drift apart. When that file is absent the builder emits ``{}`` and the
view says so; that is the only other acceptable state.
Expand All @@ -130,7 +130,7 @@ def test_research_bundle_quotes_the_measured_run():
with open(path, encoding="utf-8") as handle:
research = json.load(handle)

measured = os.path.join(REPO_ROOT, "promo", "rl-data.json")
measured = os.path.join(REPO_ROOT, ".docs", "assets", "rl-data.json")
if not research:
assert not os.path.exists(measured)
return
Expand Down Expand Up @@ -210,3 +210,46 @@ def test_bootstrap_is_valid_python():
path = os.path.join(WEB, "bootstrap.py")
with open(path, encoding="utf-8") as handle:
compile(handle.read(), path, "exec")


def test_documentation_lives_in_one_directory():
"""`.docs/` is the only documentation directory.

It used to be three — `docs/` for contributor files and README charts,
`promo/` for videos, `.docs/` for research notes — with no rule saying
which took what, so every new file was a guess. The two community files
are the deliberate exception: GitHub only recognises `CONTRIBUTING.md`
and `CODE_OF_CONDUCT.md` in the root, `.github/` or `docs/`, so filing
them under `.docs/` would drop the contributing link on the issue and
pull-request forms.
"""
for gone in ("docs", "promo"):
assert not os.path.isdir(
os.path.join(REPO_ROOT, gone)
), f"{gone}/ is back; documentation belongs in .docs/"

for name in ("README.md", "RELEASING.md", "learned-heuristics.md"):
assert os.path.exists(os.path.join(REPO_ROOT, ".docs", name))

for name in ("CONTRIBUTING.md", "CODE_OF_CONDUCT.md"):
assert os.path.exists(
os.path.join(REPO_ROOT, ".github", name)
), f"{name} must stay somewhere GitHub looks for it"


def test_the_sdist_keeps_the_data_its_tests_read():
"""The media is excluded by extension, not by excluding `.docs/assets/`.

`rl-data.json` sits beside the images and videos, and
``test_research_bundle_quotes_the_measured_run`` opens it. Excluding the
directory outright would ship an sdist whose own suite fails on a missing
file — which is exactly how the 6.6 MB sdist fix broke two parser tests.
"""
with open(os.path.join(REPO_ROOT, "pyproject.toml"), encoding="utf-8") as handle:
pyproject = handle.read()
assert '".docs/assets/*.png"' in pyproject
assert '".docs/assets/*.mp4"' in pyproject
assert '".docs/assets/**"' not in pyproject, (
"excluding the whole assets directory drops rl-data.json, "
"which the test suite reads"
)
6 changes: 3 additions & 3 deletions tools/build_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,12 @@ def collect_capabilities() -> dict:
def collect_research() -> dict:
"""Distil the learned-heuristic measurements for the Research view.

Read from ``promo/rl-data.json`` — the cache the RL promo video renders
Read from ``.docs/assets/rl-data.json`` — the cache the RL promo video renders
from — so the page and the video quote the same measured run and cannot
drift apart. Returns ``{}`` when that file is absent, and the view then
says so rather than showing numbers from nowhere.
"""
path = os.path.join(ROOT, "promo", "rl-data.json")
path = os.path.join(ROOT, ".docs", "assets", "rl-data.json")
if not os.path.exists(path):
return {}
with open(path, encoding="utf-8") as handle:
Expand Down Expand Up @@ -307,7 +307,7 @@ def main() -> int:
print(
" learned-heuristic measurements -> web/dist/research.json"
if research
else " ! no promo/rl-data.json; the Research view will say so"
else " ! no .docs/assets/rl-data.json; the Research view will say so"
)
return 0

Expand Down
8 changes: 4 additions & 4 deletions tools/make_learn_promo.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
that shaped its design, and animates whatever came back. If the method gets
better or worse, so does the video.

python tools/make_learn_promo.py -o promo/jupyddl-rl.mp4
python tools/make_learn_promo.py -o .docs/assets/jupyddl-rl.mp4

Collection takes a few minutes, so it is cached::

python tools/make_learn_promo.py --cache promo/rl-data.json # measure once
python tools/make_learn_promo.py --cache promo/rl-data.json # reuse
python tools/make_learn_promo.py --cache .docs/assets/rl-data.json # measure once
python tools/make_learn_promo.py --cache .docs/assets/rl-data.json # reuse

Needs the ``viz`` extra plus an ffmpeg binary; ``learn`` (NumPy) makes the
collection pass much faster but is not required.
Expand Down Expand Up @@ -1329,7 +1329,7 @@ def main() -> int:
parser = argparse.ArgumentParser(
description="Render the learned-heuristic / RL promo video."
)
parser.add_argument("-o", "--output", default="promo/jupyddl-rl.mp4")
parser.add_argument("-o", "--output", default=".docs/assets/jupyddl-rl.mp4")
parser.add_argument("--fps", type=int, default=FPS)
parser.add_argument("--dpi", type=int, default=DPI)
parser.add_argument(
Expand Down
4 changes: 2 additions & 2 deletions tools/make_promo.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
real planners on the bundled demo instances first, then animates what came back.
If a search gets faster or slower, the video changes with it.

python tools/make_promo.py -o promo/jupyddl.mp4
python tools/make_promo.py -o .docs/assets/jupyddl.mp4

Needs the ``viz`` extra plus an ffmpeg binary (``pip install imageio-ffmpeg``
is enough — matplotlib is pointed at the bundled static build automatically).
Expand Down Expand Up @@ -1677,7 +1677,7 @@ def render(data, out: str, screenshot=None, fps: int = FPS, dpi: int = DPI):

def main() -> int:
parser = argparse.ArgumentParser(description="Render the jupyddl promo video.")
parser.add_argument("-o", "--output", default="promo/jupyddl.mp4")
parser.add_argument("-o", "--output", default=".docs/assets/jupyddl.mp4")
parser.add_argument("--demos", default="demos")
parser.add_argument("--fps", type=int, default=FPS)
parser.add_argument("--dpi", type=int, default=DPI)
Expand Down
Loading